Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/wet-cycles-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/ui-extensions': minor
---

Added subscribable discounts api. Update the type for `data.id` to string to fix a previously incorrect type.
35 changes: 35 additions & 0 deletions packages/ui-extensions/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,3 +952,38 @@ export interface ReadonlySignalLike<T> {
*/
subscribe(fn: (value: T) => void): () => void;
}

/**
* A result type that indicates the success or failure of an operation.
*/
type Result<T> =
| {success: true; value: T}
| {success: false; errors: ValidationError[]};

/**
* A validation error object that is returned when an operation fails.
*/
interface ValidationError {
type: 'error';
/**
* A message describing the error.
*/
message: string;
/**
* A code identifier for the error.
*/
code: string;
/**
* Field-level validation issues
*/
issues?: {
message: string;
path: string[];
}[];
}

/**
* A function that updates a signal and returns a result indicating success or failure.
* The function is typically used alongisde a ReadonlySignalLike object.
*/
export type UpdateSignalFunction<T> = (value: T) => Result<T>;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ const data: ReferenceEntityTemplateSchema = {
'The object exposed to the extension that contains the discount function settings.',
type: 'DiscountFunctionSettingsData',
},
{
title: 'discounts',
description:
'The reactive API for managing discount function configuration.',
type: 'DiscountsApi',
},
],
category: 'API',
subCategory: 'Target APIs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {BlockExtensionApi} from '../block/block';
import type {ExtensionTarget as AnyExtensionTarget} from '../../extension-targets';

import {ApplyMetafieldChange} from './metafields';
import {DiscountFunctionSettingsData} from './launch-options';
import {DiscountFunctionSettingsData, DiscountsApi} from './launch-options';

export interface DiscountFunctionSettingsApi<
ExtensionTarget extends AnyExtensionTarget,
Expand All @@ -12,4 +12,5 @@ export interface DiscountFunctionSettingsApi<
*/
applyMetafieldChange: ApplyMetafieldChange;
data: DiscountFunctionSettingsData;
discounts: DiscountsApi;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type {
ReadonlySignalLike,
UpdateSignalFunction,
} from '../../../../shared';

interface Metafield {
description?: string;
id: string;
Expand All @@ -7,23 +12,56 @@ interface Metafield {
type: string;
}

export enum DiscountClass {
Product = 'PRODUCT',
Order = 'ORDER',
Shipping = 'SHIPPING',
}
type DiscountClass = 'product' | 'order' | 'shipping';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to call this out in the changset as a breaking change. We will also need to ensure backwards compatibility so that only 2026-01 and up will receive the lowercased enum.

Copy link
Contributor Author

@jonathanhamel4 jonathanhamel4 Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to document this as a breaking change? This type was defined but never used. Like the input never referenced that type 🤔 It wasn't flagged as unused in CI because it is exported I assume.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, we've never exposed this in the api, just added the enum so it's ok to not be a breaking change


type DiscountMethod = 'automatic' | 'code';

type PurchaseType = 'one_time_purchase' | 'subscription' | 'both';

interface Discount {
/**
* The object that exposes the validation with its settings.
*/
export interface DiscountFunctionSettingsData {
/**
* the discount's gid
* The unique identifier for the discount.
*/
id: string;
/**
* The discount metafields.
*/
metafields: Metafield[];
}

/**
* The object that exposes the validation with its settings.
* Reactive Api for managing discount function configuration.
*/
export interface DiscountFunctionSettingsData {
id: Discount;
Copy link
Contributor Author

@jonathanhamel4 jonathanhamel4 Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually wrong in the past. We never sent the id as an object with a single id property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please patch the previous release too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been wrong for more than one release. I can patch all of them, do I just have to merge the branch to the non-rc one? Like 2025-10, etc. Anything else I need to do?

metafields: Metafield[];
export interface DiscountsApi {
/**
* A signal that contains the discount classes.
*/
discountClasses: ReadonlySignalLike<DiscountClass[]>;
/**
* A function that updates the discount classes.
*/
updateDiscountClasses: UpdateSignalFunction<DiscountClass[]>;
/**
* A signal that contains the discount method.
*/
discountMethod: ReadonlySignalLike<DiscountMethod>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discountMethod is readonly.

/**
* A signal that contains the purchase type.
*/
purchaseType: ReadonlySignalLike<PurchaseType>;
/**
* A function that updates the purchase type.
*/
updatePurchaseType: UpdateSignalFunction<PurchaseType>;
/**
* A signal that contains the recurring cycle limit for subscriptions purchases.
*/
recurringCycleLimit: ReadonlySignalLike<number | null | undefined>;
/**
* A function that updates the recurring cycle limit for subscriptions purchases.
*/
updateRecurringCycleLimit: UpdateSignalFunction<number | null | undefined>;
}