@mpgd/adapter-devvit 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog-products.d.ts +49 -0
- package/dist/catalog-products.js +79 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/web.d.ts +28 -0
- package/dist/web.js +64 -0
- package/package.json +4 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ProductInfo, ProductType } from '@mpgd/platform';
|
|
2
|
+
import { type DevvitCommerceProduct } from './payments.js';
|
|
3
|
+
/** The catalog target whose `platformProductIds` entry is the Devvit SKU. */
|
|
4
|
+
export declare const defaultDevvitCatalogTarget = "reddit";
|
|
5
|
+
/**
|
|
6
|
+
* The product catalog fields this module reads (`ProductCatalog` from
|
|
7
|
+
* `@mpgd/catalog` fits). Structural so the adapter adds no catalog dependency.
|
|
8
|
+
*/
|
|
9
|
+
export interface DevvitCatalogProductLike {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly type: ProductType;
|
|
12
|
+
readonly platformProductIds: Readonly<Partial<Record<string, string>>>;
|
|
13
|
+
}
|
|
14
|
+
export interface DevvitCatalogLike {
|
|
15
|
+
readonly products: readonly DevvitCatalogProductLike[];
|
|
16
|
+
}
|
|
17
|
+
/** A logical product paired with its Devvit SKU, as the server bridge handler takes it. */
|
|
18
|
+
export interface DevvitCatalogBridgeProduct {
|
|
19
|
+
readonly productId: string;
|
|
20
|
+
readonly sku: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CreateDevvitCommerceProductsOptions {
|
|
23
|
+
/** Defaults to `reddit`. */
|
|
24
|
+
readonly target?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Static product info for the webview provider's `fetchProducts`. Devvit
|
|
27
|
+
* prices live products in Reddit Gold on the server (`payments.getProducts`),
|
|
28
|
+
* so the default lists the logical id as title with a Reddit Gold price
|
|
29
|
+
* placeholder; a game that wants localized copy supplies its own.
|
|
30
|
+
*/
|
|
31
|
+
readonly describe?: (product: DevvitCatalogProductLike, sku: string) => Pick<ProductInfo, 'title' | 'description' | 'price'>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The `{ productId, sku }` pairs `createDevvitCommerceV2BridgeHandler` takes,
|
|
35
|
+
* read from the catalog's `platformProductIds[target]` entries. The catalog is
|
|
36
|
+
* the single source of the Devvit SKUs and its rules apply, as
|
|
37
|
+
* `resolveProductPlatformId` reads them: the SKU is trimmed, and a product
|
|
38
|
+
* without an entry, or with a blank one, is not sold on Reddit. A subscription
|
|
39
|
+
* is skipped because Devvit sells one-time goods only. Duplicate product ids
|
|
40
|
+
* and SKUs are rejected.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createDevvitCommerceBridgeProducts(catalog: DevvitCatalogLike, target?: string): readonly DevvitCatalogBridgeProduct[];
|
|
43
|
+
/**
|
|
44
|
+
* The webview products for `createDevvitPlatformGateway({ commerce: { products } })`
|
|
45
|
+
* and `createDevvitCommerceAdapter`, derived from the same catalog entries as
|
|
46
|
+
* `createDevvitCommerceBridgeProducts`, so the webview and the server agree on
|
|
47
|
+
* every SKU.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createDevvitCommerceProducts(catalog: DevvitCatalogLike, options?: CreateDevvitCommerceProductsOptions): readonly DevvitCommerceProduct[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { normalizeDevvitCommerceProducts } from './payments.js';
|
|
2
|
+
/** The catalog target whose `platformProductIds` entry is the Devvit SKU. */
|
|
3
|
+
export const defaultDevvitCatalogTarget = 'reddit';
|
|
4
|
+
/**
|
|
5
|
+
* The `{ productId, sku }` pairs `createDevvitCommerceV2BridgeHandler` takes,
|
|
6
|
+
* read from the catalog's `platformProductIds[target]` entries. The catalog is
|
|
7
|
+
* the single source of the Devvit SKUs and its rules apply, as
|
|
8
|
+
* `resolveProductPlatformId` reads them: the SKU is trimmed, and a product
|
|
9
|
+
* without an entry, or with a blank one, is not sold on Reddit. A subscription
|
|
10
|
+
* is skipped because Devvit sells one-time goods only. Duplicate product ids
|
|
11
|
+
* and SKUs are rejected.
|
|
12
|
+
*/
|
|
13
|
+
export function createDevvitCommerceBridgeProducts(catalog, target = defaultDevvitCatalogTarget) {
|
|
14
|
+
const skus = new Set();
|
|
15
|
+
const productIds = new Set();
|
|
16
|
+
const products = [];
|
|
17
|
+
for (const product of catalog.products) {
|
|
18
|
+
const sku = readSku(product, target);
|
|
19
|
+
if (sku === undefined || product.type === 'subscription') {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (productIds.has(product.id)) {
|
|
23
|
+
throw new TypeError(`Duplicate Devvit logical product ID: ${product.id}`);
|
|
24
|
+
}
|
|
25
|
+
if (skus.has(sku)) {
|
|
26
|
+
throw new TypeError(`Duplicate Devvit product SKU: ${sku}`);
|
|
27
|
+
}
|
|
28
|
+
productIds.add(product.id);
|
|
29
|
+
skus.add(sku);
|
|
30
|
+
products.push(Object.freeze({ productId: product.id, sku }));
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze(products);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The webview products for `createDevvitPlatformGateway({ commerce: { products } })`
|
|
36
|
+
* and `createDevvitCommerceAdapter`, derived from the same catalog entries as
|
|
37
|
+
* `createDevvitCommerceBridgeProducts`, so the webview and the server agree on
|
|
38
|
+
* every SKU.
|
|
39
|
+
*/
|
|
40
|
+
export function createDevvitCommerceProducts(catalog, options = {}) {
|
|
41
|
+
const target = options.target ?? defaultDevvitCatalogTarget;
|
|
42
|
+
const describe = options.describe ?? describeDevvitProduct;
|
|
43
|
+
const bridgeProducts = createDevvitCommerceBridgeProducts(catalog, target);
|
|
44
|
+
const byId = new Map(catalog.products.map((product) => [product.id, product]));
|
|
45
|
+
return normalizeDevvitCommerceProducts(bridgeProducts.map((bridgeProduct) => {
|
|
46
|
+
const product = byId.get(bridgeProduct.productId);
|
|
47
|
+
if (product === undefined) {
|
|
48
|
+
throw new TypeError(`Devvit product ${bridgeProduct.productId} is missing from the catalog.`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
info: {
|
|
52
|
+
id: product.id,
|
|
53
|
+
type: product.type,
|
|
54
|
+
...describe(product, bridgeProduct.sku),
|
|
55
|
+
},
|
|
56
|
+
sku: bridgeProduct.sku,
|
|
57
|
+
};
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
function describeDevvitProduct(product) {
|
|
61
|
+
return {
|
|
62
|
+
title: product.id,
|
|
63
|
+
description: '',
|
|
64
|
+
price: { formatted: 'Reddit Gold', currencyCode: 'GOLD' },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The catalog's reading of a platform id: own key only, trimmed, blank as unsold. */
|
|
68
|
+
function readSku(product, target) {
|
|
69
|
+
const identifiers = product.platformProductIds;
|
|
70
|
+
if (!Object.prototype.hasOwnProperty.call(identifiers, target)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const value = identifiers[target];
|
|
74
|
+
if (typeof value !== 'string') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const sku = value.trim();
|
|
78
|
+
return sku.length === 0 ? undefined : sku;
|
|
79
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type CreateDevvitCommerceProviderInput, type DevvitOrderSource } from '
|
|
|
5
5
|
export * from './commerce-contract.js';
|
|
6
6
|
export * from './commerce-provider.js';
|
|
7
7
|
export * from './payments.js';
|
|
8
|
+
export * from './catalog-products.js';
|
|
8
9
|
export declare const defaultDevvitRpcEndpoint = "/api/mpgd/rpc";
|
|
9
10
|
/** Bridge error of `createDevvitOrpcBridge` when the oRPC request itself failed. */
|
|
10
11
|
export declare const devvitBridgeNetworkErrorCode: 'DEVVIT_BRIDGE_NETWORK_ERROR';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createDevvitCommerceProvider, } from './commerce-provider.js';
|
|
|
7
7
|
export * from './commerce-contract.js';
|
|
8
8
|
export * from './commerce-provider.js';
|
|
9
9
|
export * from './payments.js';
|
|
10
|
+
export * from './catalog-products.js';
|
|
10
11
|
export const defaultDevvitRpcEndpoint = defaultBridgeRpcEndpoint;
|
|
11
12
|
/** Bridge error of `createDevvitOrpcBridge` when the oRPC request itself failed. */
|
|
12
13
|
export const devvitBridgeNetworkErrorCode = 'DEVVIT_BRIDGE_NETWORK_ERROR';
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './commerce-bridge.js';
|
|
2
|
+
export { createDevvitCommerceBridgeProducts, defaultDevvitCatalogTarget, type DevvitCatalogBridgeProduct, type DevvitCatalogLike, type DevvitCatalogProductLike, } from '../catalog-products.js';
|
|
2
3
|
export * from './payment-order.js';
|
|
3
4
|
export * from './post-operation.js';
|
|
4
5
|
export * from './redis-post-operation-store.js';
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './commerce-bridge.js';
|
|
2
|
+
export { createDevvitCommerceBridgeProducts, defaultDevvitCatalogTarget, } from '../catalog-products.js';
|
|
2
3
|
export * from './payment-order.js';
|
|
3
4
|
export * from './post-operation.js';
|
|
4
5
|
export * from './redis-post-operation-store.js';
|
package/dist/web.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShareResult } from '@mpgd/platform';
|
|
2
|
+
import type { DevvitCheckoutResult, DevvitPaymentsClient } from './payments.js';
|
|
2
3
|
import { type DevvitSurfaceOptions, type DevvitSurfaceResult } from './surface.js';
|
|
3
4
|
import { type DevvitPreviewViewModeOptions, type DevvitPreviewViewModeResult, type DevvitViewModeOptions, type DevvitViewModeResult } from './view-mode.js';
|
|
4
5
|
export type { DevvitSurfaceClient, DevvitSurfaceMode, DevvitSurfaceOptions, DevvitSurfaceResult, } from './surface.js';
|
|
@@ -30,3 +31,30 @@ export declare function requestDevvitExpandedMode(event: MouseEvent, entry: stri
|
|
|
30
31
|
* completed sharing. Devvit does not report a completion callback.
|
|
31
32
|
*/
|
|
32
33
|
export declare function presentDevvitShareSheet(options: DevvitShareSheetOptions): Promise<ShareResult>;
|
|
34
|
+
/** `OrderResultEvent` of `@devvit/payments/client`, as the webview reads it. */
|
|
35
|
+
export interface DevvitOrderResultEventLike {
|
|
36
|
+
/** `OrderResultStatus`: 0 cancelled, 1 success, 2 error (or its name). */
|
|
37
|
+
readonly status: number | string;
|
|
38
|
+
readonly orderId?: string | undefined;
|
|
39
|
+
readonly errorCode?: number | undefined;
|
|
40
|
+
readonly errorMessage?: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
/** The checkout half of `DevvitPaymentsClient`; the v2 provider needs nothing else. */
|
|
43
|
+
export type DevvitWebPaymentsClient = Pick<DevvitPaymentsClient, 'purchase'>;
|
|
44
|
+
/**
|
|
45
|
+
* Maps one Reddit checkout answer to the adapter's `DevvitCheckoutResult`:
|
|
46
|
+
* `STATUS_SUCCESS` with an order id is `completed` (the order id is Reddit's;
|
|
47
|
+
* the backend ledger decides the grant), `STATUS_SUCCESS` without one is
|
|
48
|
+
* `pending` so the reconciler resolves it from the paid-order list,
|
|
49
|
+
* `STATUS_CANCELLED` is `cancelled`, and `STATUS_ERROR` is `failed` with the
|
|
50
|
+
* platform code kept as `storeCode`. An unknown status is `pending`: an order
|
|
51
|
+
* may exist and nothing is granted or dropped on the client.
|
|
52
|
+
*/
|
|
53
|
+
export declare function toDevvitCheckoutResult(event: DevvitOrderResultEventLike): DevvitCheckoutResult;
|
|
54
|
+
/**
|
|
55
|
+
* `purchase()` from `@devvit/payments/client` (re-exported by
|
|
56
|
+
* `@devvit/web/client`) as the checkout client of `createDevvitPlatformGateway({
|
|
57
|
+
* commerce: { client } })`. One SKU per checkout, no client metadata: Reddit's
|
|
58
|
+
* order id is the only identity the purchase carries.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createDevvitWebPaymentsClient(): DevvitWebPaymentsClient;
|
package/dist/web.js
CHANGED
|
@@ -65,3 +65,67 @@ function isAbortError(error) {
|
|
|
65
65
|
&& error !== null
|
|
66
66
|
&& error.name === 'AbortError';
|
|
67
67
|
}
|
|
68
|
+
const successStatuses = new Set([1, 'STATUS_SUCCESS', 'SUCCESS']);
|
|
69
|
+
const cancelledStatuses = new Set([
|
|
70
|
+
0,
|
|
71
|
+
'STATUS_CANCELLED',
|
|
72
|
+
'CANCELLED',
|
|
73
|
+
]);
|
|
74
|
+
const errorStatuses = new Set([2, 'STATUS_ERROR', 'ERROR']);
|
|
75
|
+
/**
|
|
76
|
+
* Maps one Reddit checkout answer to the adapter's `DevvitCheckoutResult`:
|
|
77
|
+
* `STATUS_SUCCESS` with an order id is `completed` (the order id is Reddit's;
|
|
78
|
+
* the backend ledger decides the grant), `STATUS_SUCCESS` without one is
|
|
79
|
+
* `pending` so the reconciler resolves it from the paid-order list,
|
|
80
|
+
* `STATUS_CANCELLED` is `cancelled`, and `STATUS_ERROR` is `failed` with the
|
|
81
|
+
* platform code kept as `storeCode`. An unknown status is `pending`: an order
|
|
82
|
+
* may exist and nothing is granted or dropped on the client.
|
|
83
|
+
*/
|
|
84
|
+
export function toDevvitCheckoutResult(event) {
|
|
85
|
+
const orderId = typeof event.orderId === 'string' && event.orderId.trim().length > 0
|
|
86
|
+
? event.orderId.trim()
|
|
87
|
+
: undefined;
|
|
88
|
+
if (successStatuses.has(event.status)) {
|
|
89
|
+
return orderId === undefined ? { status: 'pending' } : { status: 'completed', orderId };
|
|
90
|
+
}
|
|
91
|
+
if (cancelledStatuses.has(event.status)) {
|
|
92
|
+
return {
|
|
93
|
+
status: 'cancelled',
|
|
94
|
+
...(orderId === undefined ? {} : { orderId }),
|
|
95
|
+
error: { code: 'USER_CANCELLED', storeCode: 'DEVVIT_CHECKOUT_CANCELLED', retryable: false },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (errorStatuses.has(event.status)) {
|
|
99
|
+
const storeCode = event.errorCode === undefined
|
|
100
|
+
? 'DEVVIT_CHECKOUT_FAILED'
|
|
101
|
+
: String(event.errorCode);
|
|
102
|
+
return {
|
|
103
|
+
status: 'failed',
|
|
104
|
+
...(orderId === undefined ? {} : { orderId }),
|
|
105
|
+
error: {
|
|
106
|
+
code: storeCode,
|
|
107
|
+
storeCode,
|
|
108
|
+
...(typeof event.errorMessage === 'string' && event.errorMessage.length > 0
|
|
109
|
+
? { message: event.errorMessage }
|
|
110
|
+
: {}),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { status: 'pending', ...(orderId === undefined ? {} : { orderId }) };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* `purchase()` from `@devvit/payments/client` (re-exported by
|
|
118
|
+
* `@devvit/web/client`) as the checkout client of `createDevvitPlatformGateway({
|
|
119
|
+
* commerce: { client } })`. One SKU per checkout, no client metadata: Reddit's
|
|
120
|
+
* order id is the only identity the purchase carries.
|
|
121
|
+
*/
|
|
122
|
+
export function createDevvitWebPaymentsClient() {
|
|
123
|
+
return {
|
|
124
|
+
async purchase(sku) {
|
|
125
|
+
if (typeof sku !== 'string' || sku.trim().length === 0) {
|
|
126
|
+
throw new TypeError('Devvit checkout requires a non-empty SKU.');
|
|
127
|
+
}
|
|
128
|
+
return toDevvitCheckoutResult(await browserClient.purchase(sku));
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-devvit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Reddit Devvit Web platform adapter for mpgd games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@devvit/web": "0.14.1",
|
|
58
|
-
"@mpgd/
|
|
59
|
-
"@mpgd/
|
|
60
|
-
"@mpgd/platform": "0.
|
|
58
|
+
"@mpgd/game-services": "0.23.0",
|
|
59
|
+
"@mpgd/bridge": "0.12.3",
|
|
60
|
+
"@mpgd/platform": "0.22.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^24.0.0",
|