client, clientReadOnly, server

This commit is contained in:
Bilal Godil 2025-10-17 14:51:57 -07:00
parent d27052b79c
commit 20c9691d4f
5 changed files with 91 additions and 23 deletions

View File

@ -62,7 +62,9 @@ export async function ensureProductIdOrInlineProduct(
freeTrial: value.free_trial,
serverOnly: true,
}])),
metadata: inlineProduct.metadata,
clientMetadata: inlineProduct.client_metadata ?? undefined,
clientReadOnlyMetadata: inlineProduct.client_read_only_metadata ?? undefined,
serverMetadata: inlineProduct.server_metadata ?? undefined,
includedItems: typedFromEntries(Object.entries(inlineProduct.included_items).map(([key, value]) => [key, {
repeat: value.repeat ?? "never",
quantity: value.quantity ?? 0,
@ -429,7 +431,9 @@ export function productToInlineProduct(product: ProductWithMetadata): yup.InferT
stackable: product.stackable === true,
server_only: product.serverOnly === true,
included_items: product.includedItems,
metadata: product.metadata,
client_metadata: product.clientMetadata ?? null,
client_read_only_metadata: product.clientReadOnlyMetadata ?? null,
server_metadata: product.serverMetadata ?? null,
prices: product.prices === "include-by-default" ? {} : typedFromEntries(typedEntries(product.prices).map(([key, value]) => [key, filterUndefined({
...typedFromEntries(SUPPORTED_CURRENCIES.map(c => [c.code, getOrUndefined(value, c.code)])),
interval: value.interval,

View File

@ -300,7 +300,7 @@ it("should return inline product metadata when validating purchase code", async
},
},
included_items: {},
metadata: {
server_metadata: {
reference_id: "ref-123",
features: ["priority-support", "analytics"],
},
@ -320,15 +320,43 @@ it("should return inline product metadata when validating purchase code", async
full_code: fullCode,
},
});
expect(validateResponse.status).toBe(200);
const validateBody = validateResponse.body;
expect(validateBody.product.metadata).toMatchInlineSnapshot(`
{
"features": [
"priority-support",
"analytics",
],
"reference_id": "ref-123",
expect(validateResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"already_bought_non_stackable": false,
"charges_enabled": false,
"conflicting_products": [],
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Metadata Inline Product",
"included_items": {},
"prices": {
"monthly-metadata": {
"USD": "1500",
"interval": [
1,
"month",
],
},
},
"server_metadata": {
"features": [
"priority-support",
"analytics",
],
"reference_id": "ref-123",
},
"server_only": true,
"stackable": false,
},
"project_id": "<stripped UUID>",
"stripe_account_id": <stripped field 'stripe_account_id'>,
"test_mode": true,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
});

View File

@ -108,6 +108,8 @@ it("should grant configured subscription product and expose it via listing", asy
{
"id": "pro-plan",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Pro Plan",
"included_items": {},
@ -120,6 +122,7 @@ it("should grant configured subscription product and expose it via listing", asy
],
},
},
"server_metadata": null,
"server_only": false,
"stackable": false,
},
@ -200,6 +203,8 @@ it("should hide server-only products from clients while exposing them to servers
{
"id": "server-plan",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Server Plan",
"included_items": {},
@ -212,6 +217,7 @@ it("should hide server-only products from clients while exposing them to servers
],
},
},
"server_metadata": null,
"server_only": true,
"stackable": false,
},
@ -327,6 +333,8 @@ it("should allow granting stackable product with custom quantity", async ({ expe
{
"id": "stackable-plan",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Stackable Plan",
"included_items": {},
@ -339,6 +347,7 @@ it("should allow granting stackable product with custom quantity", async ({ expe
],
},
},
"server_metadata": null,
"server_only": false,
"stackable": true,
},
@ -372,7 +381,7 @@ it("should grant inline product without needing configuration", async ({ expect
},
},
included_items: {},
metadata: {
server_metadata: {
cohort: "beta",
flags: ["inline-grant"],
},
@ -393,13 +402,11 @@ it("should grant inline product without needing configuration", async ({ expect
{
"id": null,
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Inline Access",
"included_items": {},
"metadata": {
"cohort": "beta",
"flags": ["inline-grant"],
},
"prices": {
"quarterly": {
"USD": "2400",
@ -409,6 +416,10 @@ it("should grant inline product without needing configuration", async ({ expect
],
},
},
"server_metadata": {
"cohort": "beta",
"flags": ["inline-grant"],
},
"server_only": true,
"stackable": false,
},
@ -693,6 +704,8 @@ it("listing products should list both subscription and one-time products", async
{
"id": "subscription-plan",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Subscription Plan",
"included_items": {},
@ -705,6 +718,7 @@ it("listing products should list both subscription and one-time products", async
],
},
},
"server_metadata": null,
"server_only": false,
"stackable": false,
},
@ -713,10 +727,13 @@ it("listing products should list both subscription and one-time products", async
{
"id": "lifetime-addon",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Lifetime Add-on",
"included_items": {},
"prices": { "lifetime": { "USD": "5000" } },
"server_metadata": null,
"server_only": false,
"stackable": false,
},
@ -821,6 +838,8 @@ it("listing products should support cursor pagination", async ({ expect }) => {
{
"id": "subscription-plan",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Subscription Plan",
"included_items": {},
@ -833,6 +852,7 @@ it("listing products should support cursor pagination", async ({ expect }) => {
],
},
},
"server_metadata": null,
"server_only": false,
"stackable": false,
},
@ -858,10 +878,13 @@ it("listing products should support cursor pagination", async ({ expect }) => {
{
"id": "lifetime-addon",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Lifetime Add-on",
"included_items": {},
"prices": { "lifetime": { "USD": "5000" } },
"server_metadata": null,
"server_only": false,
"stackable": false,
},
@ -870,10 +893,13 @@ it("listing products should support cursor pagination", async ({ expect }) => {
{
"id": "pro-addon",
"product": {
"client_metadata": null,
"client_read_only_metadata": null,
"customer_type": "user",
"display_name": "Pro Add-on",
"included_items": {},
"prices": { "standard": { "USD": "7000" } },
"server_metadata": null,
"server_only": false,
"stackable": false,
},

View File

@ -463,7 +463,7 @@ it("should list inline product metadata after completing test-mode purchase", as
},
},
included_items: {},
metadata: {
server_metadata: {
correlation_id: "inline-test-123",
attributes: {
seats: 5,
@ -495,10 +495,10 @@ it("should list inline product metadata after completing test-mode purchase", as
});
expect(listResponse.status).toBe(200);
const listBody = listResponse.body as {
items: Array<{ product: { metadata?: Record<string, unknown> } }>,
items: Array<{ product: { server_metadata?: Record<string, unknown> } }>,
};
expect(listBody.items).toHaveLength(1);
expect(listBody.items[0].product.metadata).toMatchInlineSnapshot(`
expect(listBody.items[0].product.server_metadata).toMatchInlineSnapshot(`
{
"attributes": {
"seats": 5,

View File

@ -591,9 +591,17 @@ export const productSchema = yupObject({
),
});
const productMetadata = jsonSchema.optional().meta({ openapiField: { description: 'Optional metadata that Stack Auth will store and return with this product. Use this to attach custom data needed by your application.', exampleValue: { featureFlag: true, source: 'marketing-campaign' } } });
const productMetadataExample = { featureFlag: true, source: 'marketing-campaign' } as const;
export const productSchemaWithMetadata = productSchema.concat(yupObject({ metadata: productMetadata }));
export const productClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('product'), exampleValue: productMetadataExample } });
export const productClientReadOnlyMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientReadOnlyMetaDataDescription('product'), exampleValue: productMetadataExample } });
export const productServerMetadataSchema = jsonSchema.meta({ openapiField: { description: _serverMetaDataDescription('product'), exampleValue: productMetadataExample } });
export const productSchemaWithMetadata = productSchema.concat(yupObject({
clientMetadata: productClientMetadataSchema.optional(),
clientReadOnlyMetadata: productClientReadOnlyMetadataSchema.optional(),
serverMetadata: productServerMetadataSchema.optional(),
}));
export const inlineProductSchema = yupObject({
display_name: yupString().defined(),
@ -617,7 +625,9 @@ export const inlineProductSchema = yupObject({
expires: yupString().oneOf(['never', 'when-purchase-expires', 'when-repeated']).optional(),
}),
),
metadata: productMetadata,
client_metadata: productClientMetadataSchema.optional(),
client_read_only_metadata: productClientReadOnlyMetadataSchema.optional(),
server_metadata: productServerMetadataSchema.optional(),
});
// Users