Skip to content

Commit 7699a4f

Browse files
DR-370 update payments docs referencing Ddevvit singleton (#67)
## 💸 TL;DR <!-- What's the three sentence summary of purpose of the PR --> This updates payments docs referencing Devvit Singleton <!-- Add additional details required for the PR: breaking changes, screenshots, external dependency changes --> This PR used cursor pretty heavily I read through the changes but @RarerAirError just a heads up. ## 🧪 Testing Steps / Validation <!-- add details on how this PR has been tested, include reproductions and screenshots where applicable --> ## ✅ Checks <!-- Make sure your pr passes the CI checks and do check the following fields as needed - --> - [ ] CI tests (if present) are passing - [ ] Adheres to code style for repo - [ ] Contributor License Agreement (CLA) completed if not a Reddit employee
1 parent 3484e57 commit 7699a4f

6 files changed

Lines changed: 244 additions & 314 deletions

File tree

docs/earn-money/payments/payments_add.mdx

Lines changed: 49 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -255,31 +255,13 @@ If you don’t provide an image, the default Reddit product image is used.
255255

256256
### Purchase buttons (required)
257257

258-
#### Blocks
258+
#### Devvit Web
259259

260-
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.
260+
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).
261261

262-
**Usage:**
262+
#### Blocks (legacy)
263263

264-
```tsx
265-
<ProductButton
266-
showIcon
267-
product={product}
268-
onPress={(p) => payments.purchase(p.sku)}
269-
appearance="tile"
270-
/>
271-
```
272-
273-
##### `ProductButtonProps`
274-
275-
| **Prop Name** | **Type** | **Description** |
276-
| ------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------ |
277-
| `product` | `Product` | The product object containing details such as `sku`, `price`, and `metadata`. |
278-
| `onPress` | `(product: Product) => void` | Callback function triggered when the button is pressed. |
279-
| `showIcon` | `boolean` | Determines whether the product icon is displayed on the button. Defaults to `false`. |
280-
| `appearance` | `'compact'` &#124; `'detailed'` &#124; `'tile'` | Defines the visual style of the button. Defaults to `compact`. |
281-
| `buttonAppearance` | `string` | Optional [button appearance](../../blocks/button.mdx#appearance). |
282-
| `textColor` | `string` | Optional [text color](../../blocks/text.mdx#color). |
264+
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`).
283265

284266
#### Webviews
285267

@@ -297,36 +279,30 @@ Use a consistent and clear product component to display paid goods or services t
297279

298280
## Complete the payment flow
299281

300-
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.
282+
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.
301283

302-
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.
284+
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.
303285

304-
This example shows how to issue an "extra life" to a user when they purchase the "extra_life" product.
286+
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`):
305287

306-
```ts
307-
import { type Context } from "@devvit/public-api";
308-
import { addPaymentHandler } from "@devvit/payments";
309-
import { Devvit, useState } from "@devvit/public-api";
310-
311-
Devvit.configure({
312-
redis: true,
313-
redditAPI: true,
314-
});
288+
```tsx title="server/index.ts"
289+
import type { PaymentHandlerResponse, Order } from "@devvit/web/server";
290+
import { redis } from "@devvit/web/server";
315291

316292
const GOD_MODE_SKU = "god_mode";
317293

318-
addPaymentHandler({
319-
fulfillOrder: async (order, ctx) => {
320-
if (!order.products.some(({ sku }) => sku === GOD_MODE_SKU)) {
321-
throw new Error("Unable to fulfill order: sku not found");
322-
}
323-
if (order.status !== "PAID") {
324-
throw new Error("Becoming a god has a cost (in Reddit Gold)");
325-
}
294+
app.post("/internal/payments/fulfill", async (c) => {
295+
const order = await c.req.json<Order>();
296+
if (!order.products.some((p) => p.sku === GOD_MODE_SKU)) {
297+
return c.json<PaymentHandlerResponse>({ success: false, reason: "Unable to fulfill order: sku not found" });
298+
}
299+
if (order.status !== "PAID") {
300+
return c.json<PaymentHandlerResponse>({ success: false, reason: "Becoming a god has a cost (in Reddit Gold)" });
301+
}
326302

327-
const redisKey = godModeRedisKey(ctx.postId, ctx.userId);
328-
await ctx.redis.set(redisKey, "true");
329-
},
303+
const redisKey = `post:${order.postId}:user:${order.userId}:god_mode`;
304+
await redis.set(redisKey, "true");
305+
return c.json<PaymentHandlerResponse>({ success: true });
330306
});
331307
```
332308

@@ -336,61 +312,44 @@ The frontend and backend of your app coordinate order processing.
336312

337313
![Order workflow diagram](../../assets/payments_order_flow_diagram.png)
338314

339-
To launch the payment flow, create a hook with `usePayments()` followed by `hook.purchase()` to initiate the purchase from the frontend.
315+
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).
340316

341-
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.
317+
### Get your product details
342318

343-
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.
319+
**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.
344320

345-
### Get your product details
321+
**Client:** Fetch product metadata from your API and use it to display products and call `purchase(sku)`:
346322

347-
Use the `useProducts` hook or `getProducts` function to fetch details about products.
348-
349-
```tsx
350-
import { useProducts } from "@devvit/payments";
351-
352-
export function ProductsList(context: Devvit.Context): JSX.Element {
353-
// Only query for products with the metadata "category" of value "powerup".
354-
// The metadata field can be empty - if it is, useProducts will not filter on metadata.
355-
const { products } = useProducts(context, {
356-
metadata: {
357-
category: "powerup",
358-
},
359-
});
360-
361-
return (
362-
<vstack>
363-
{products.map((product) => (
364-
<hstack>
365-
<text>{product.name}</text>
366-
<text>{product.price}</text>
367-
</hstack>
368-
))}
369-
</vstack>
370-
);
371-
}
372-
```
323+
```tsx title="client/index.ts"
324+
import { purchase, OrderResultStatus } from "@devvit/web/client";
373325

374-
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.
326+
// Fetch products from your server endpoint
327+
const products = await fetch("/api/products").then((r) => r.json());
375328

376-
```tsx
377-
import { getProducts } from '@devvit/payments';
378-
const products = await getProducts({,
379-
});
329+
// Render your UI; when user chooses a product:
330+
async function handleBuy(sku: string) {
331+
const result = await purchase(sku);
332+
if (result.status === OrderResultStatus.STATUS_SUCCESS) {
333+
// show success
334+
} else {
335+
// show error or retry (result.errorMessage may be set)
336+
}
337+
}
380338
```
381339

382340
### Initiate orders
383341

384-
Provide the product sku to trigger a purchase. This automatically populates the most recently-approved product metadata for that product id.
385-
386-
**Example**
387-
388-
```tsx
389-
import { usePayments } from '@devvit/payments';
342+
Provide the product SKU to trigger a purchase. Use `purchase(sku)` from `@devvit/web/client`; the result indicates success or failure.
390343

391-
// handles purchase results
392-
const payments = usePayments((result: OnPurchaseResult) => { console.log('Tried to buy:', result.sku, '; result:', result.status); });
344+
```tsx title="client/index.ts"
345+
import { purchase, OrderResultStatus } from "@devvit/web/client";
393346

394-
// for each sku in products:
395-
<button onPress{payments.purchase(sku)}>Buy a {sku}</button>
347+
export async function buy(sku: string) {
348+
const result = await purchase(sku);
349+
if (result.status === OrderResultStatus.STATUS_SUCCESS) {
350+
// show success
351+
} else {
352+
// show error or retry (result.errorMessage may be set)
353+
}
354+
}
396355
```

docs/earn-money/payments/payments_manage.md

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,23 @@ Once your app and products have been approved, you’re ready to use Reddit’s
44

55
## Check orders
66

7-
Reddit keeps track of historical purchases and lets you query user purchases.
7+
Reddit keeps track of historical purchases and lets you query orders.
88

9-
Orders are returned in reverse chronological order and can be filtered based on user, product, success state, or other attributes.
9+
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.
1010

11-
**Example**
11+
**Example (server):** expose orders for the current user so the client can show "Purchased!" or a purchase button.
1212

13-
```tsx
14-
import { useOrders, OrderStatus } from '@devvit/payments';
13+
```tsx title="server/index.ts"
14+
import { payments } from "@devvit/web/server";
1515

16-
export function CosmicSwordShop(context: Devvit.Context): JSX.Element {
17-
const { orders } = useOrders(context, {
18-
sku: 'cosmic_sword',
19-
});
20-
21-
// if the user hasn’t already bought the cosmic sword
22-
// then show them the purchase button
23-
if (orders.length > 0) {
24-
return <text>Purchased!</text>;
25-
} else {
26-
return <button onPress={/* Trigger purchase */}>Buy Cosmic Sword</button>;
27-
}
28-
}
16+
app.get("/api/orders", async (c) => {
17+
const orders = await payments.getOrders({ sku: "cosmic_sword" });
18+
return c.json(orders);
19+
});
2920
```
3021

22+
**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`.
23+
3124
## Update products
3225

3326
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.
3831

3932
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).
4033

41-
When a transaction is reversed for any reason, you may optionally revoke product functionality from the user by adding a `refundOrder` handler.
42-
43-
**Example**
44-
45-
```tsx
46-
addPaymentHandler({
47-
fulfillOrder: async (order: Order, ctx: Context) => {
48-
// Snip order fulfillment
49-
},
50-
refundOrder: async (order: Order, ctx: Context) => {
51-
// check if the order contains an extra life
52-
if (order.products.some(({ sku }) => sku === GOD_MODE_SKU)) {
53-
// redis key for storing number of lives user has left
54-
const livesKey = `${ctx.userId}:lives`;
55-
56-
// if so, decrement the number of lives
57-
await ctx.redis.incrBy(livesKey, -1);
58-
}
59-
},
34+
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`).
35+
36+
**Example (Devvit Web):** in your server’s refund endpoint, revoke the entitlement (e.g. decrement lives in Redis).
37+
38+
```tsx title="server/index.ts"
39+
import type { PaymentHandlerResponse, Order } from "@devvit/web/server";
40+
import { redis } from "@devvit/web/server";
41+
42+
const GOD_MODE_SKU = "god_mode";
43+
44+
app.post("/internal/payments/refund", async (c) => {
45+
const order = await c.req.json<Order>();
46+
if (order.products.some((p) => p.sku === GOD_MODE_SKU)) {
47+
const livesKey = `${order.userId}:lives`;
48+
await redis.incrBy(livesKey, -1);
49+
}
50+
return c.json<PaymentHandlerResponse>({ success: true });
6051
});
6152
```
6253

docs/earn-money/payments/support_this_app.md

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,50 @@ devvit products add support-app
1919

2020
### Add a payment handler
2121

22-
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:
22+
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`.
2323

24-
```tsx
25-
addPaymentHandler({
26-
fulfillOrder: async (order, context) => {
27-
const username = await context.reddit.getCurrentUsername();
28-
if (!username) {
29-
throw new Error("User not found");
30-
}
31-
32-
const subredditName = await context.reddit.getCurrentSubredditName();
33-
34-
await context.reddit.setUserFlair({
35-
text: "Super Duper User",
36-
subredditName,
37-
username,
38-
backgroundColor: "#ffbea6",
39-
textColor: "dark",
40-
});
41-
},
24+
Example: award custom user flair when a user completes a support purchase:
25+
26+
```tsx title="server/index.ts"
27+
import type { PaymentHandlerResponse, Order } from "@devvit/web/server";
28+
import { reddit } from "@devvit/web/server";
29+
30+
app.post("/internal/payments/fulfill", async (c) => {
31+
const order = await c.req.json<Order>();
32+
const username = order.userId; // or the username field on the order
33+
if (!username) {
34+
return c.json<PaymentHandlerResponse>({ success: false, reason: "User not found" });
35+
}
36+
37+
const subredditName = order.subredditName ?? order.subredditId;
38+
39+
await reddit.setUserFlair({
40+
text: "Super Duper User",
41+
subredditName,
42+
username,
43+
backgroundColor: "#ffbea6",
44+
textColor: "dark",
45+
});
46+
47+
return c.json<PaymentHandlerResponse>({ success: true });
4248
});
4349
```
4450

4551
### Initiate purchases
4652

47-
Next you need to provide a way for users to support your app:
53+
Provide a way for users to support your app from your client:
4854

49-
- If you use Devvit blocks, you can use the ProductButton helper to render a purchase button.
50-
- 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).
55+
- **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.
56+
- Follow the [design guidelines](./payments_add.mdx#design-guidelines) when [initiating purchases](./payments_add.mdx#initiate-orders).
5157

5258
![Support App Example](../../assets/support_this_app.png)
5359

54-
Here's how you create a ProductButton in blocks:
60+
Example client code:
5561

56-
```tsx
57-
import { usePayments, useProducts } from '@devvit/payments';
58-
import { ProductButton } from '@devvit/payments/helpers/ProductButton';
59-
import { Devvit } from '@devvit/public-api';
62+
```tsx title="client/index.ts"
63+
import { purchase, OrderResultStatus } from "@devvit/web/client";
6064

65+
<<<<<<< HEAD
6166
// addCustomPostType() is deprecated and will be unsupported. It will not work after June 30. View the announcement below this example.
6267
Devvit.addCustomPostType({
6368
render: (context) => {
@@ -82,6 +87,16 @@ Devvit.addCustomPostType({
8287
/>
8388
);
8489
})
90+
=======
91+
async function handleSupportApp() {
92+
const result = await purchase("support-app");
93+
if (result.status === OrderResultStatus.STATUS_SUCCESS) {
94+
// show success, e.g. toast: "Thanks for your support!"
95+
} else {
96+
// show error or retry (result.errorMessage may be set)
97+
}
98+
}
99+
>>>>>>> 64da331 (DR-370 update payments docs referencing Ddevvit singleton)
85100
```
86101
[View `addCustomPostType` deprecation announcement.](https://www.reddit.com/r/Devvit/comments/1r3xcm2/devvit_web_and_the_future_of_devvit/)
87102

0 commit comments

Comments
 (0)