@jarwizz/create-jarshop 0.1.2 → 0.1.3

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.
Files changed (31) hide show
  1. package/README.md +2 -2
  2. package/dist/generator.js +4 -1
  3. package/dist/generator.js.map +1 -1
  4. package/dist/template/AGENTS.md +3 -3
  5. package/dist/template/CONTEXT.md +4 -0
  6. package/dist/template/README.md +5 -0
  7. package/dist/template/package.json +6 -1
  8. package/dist/template/pnpm-lock.yaml +2339 -79
  9. package/dist/template/pnpm-workspace.yaml +12 -1
  10. package/dist/template/src/plugins/jarshop-checkout/cod-payment.test.ts +44 -0
  11. package/dist/template/src/plugins/jarshop-checkout/cod-payment.ts +28 -0
  12. package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +37 -4
  13. package/dist/template/src/plugins/jarshop-checkout/index.ts +5 -1
  14. package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +80 -23
  15. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +1 -1
  16. package/dist/template/src/seed.ts +41 -19
  17. package/dist/template/src/vendure-config.ts +5 -3
  18. package/dist/template/storefront/app/[locale]/checkout/confirmation/route.ts +21 -0
  19. package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +6 -1
  20. package/dist/template/storefront/app/[locale]/page.tsx +2 -2
  21. package/dist/template/storefront/eslint.config.mjs +8 -1
  22. package/dist/template/storefront/lib/checkout.ts +14 -8
  23. package/dist/template/storefront/lib/commerce.ts +7 -3
  24. package/dist/template/storefront/lib/confirmation.ts +66 -18
  25. package/dist/template/storefront/package.json +7 -3
  26. package/dist/template/storefront/vitest.config.ts +8 -0
  27. package/dist/template/turbo.json +7 -0
  28. package/dist/template-manifest.json +1 -1
  29. package/package.json +1 -1
  30. package/dist/template/storefront/pnpm-lock.yaml +0 -4316
  31. package/dist/template/storefront/pnpm-workspace.yaml +0 -26
@@ -1,4 +1,15 @@
1
- packages: []
1
+ packages:
2
+ - .
3
+ - storefront
4
+
5
+ # The eslint-config-next range otherwise resolves a newer unreviewed resolver,
6
+ # which pnpm rejects as a trust downgrade. Keep the reviewed compatible
7
+ # resolver exact for the whole Client Project workspace.
8
+ overrides:
9
+ eslint-import-resolver-typescript: 3.8.7
10
+
11
+ minimumReleaseAgeExclude:
12
+ - "@jarwizz/commerce-sdk@0.3.0"
2
13
 
3
14
  allowBuilds:
4
15
  bcrypt: true
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type {
3
+ Order,
4
+ Payment,
5
+ PaymentMethod,
6
+ RequestContext,
7
+ } from "@vendure/core";
8
+ import { jarShopCodPaymentHandler } from "./cod-payment.js";
9
+
10
+ const context = {} as RequestContext;
11
+ const order = { code: "ORDER-123" } as Order;
12
+ const method = {} as PaymentMethod;
13
+
14
+ describe("jarShopCodPaymentHandler", () => {
15
+ it("authorizes COD without pretending that money was collected", async () => {
16
+ const result = await jarShopCodPaymentHandler.createPayment(
17
+ context,
18
+ order,
19
+ 12_345,
20
+ [],
21
+ { attemptId: "attempt-1" },
22
+ method,
23
+ );
24
+
25
+ expect(result).toMatchObject({
26
+ amount: 12_345,
27
+ state: "Authorized",
28
+ transactionId: "cod:ORDER-123",
29
+ method: "jarshop-cod-handler",
30
+ });
31
+ });
32
+
33
+ it("allows an operator-driven settlement after collection", async () => {
34
+ const result = await jarShopCodPaymentHandler.settlePayment(
35
+ context,
36
+ order,
37
+ {} as Payment,
38
+ [],
39
+ method,
40
+ );
41
+
42
+ expect(result).toEqual({ success: true });
43
+ });
44
+ });
@@ -0,0 +1,28 @@
1
+ import { LanguageCode, PaymentMethodHandler } from "@vendure/core";
2
+
3
+ /**
4
+ * Cash on delivery is a two-step payment: checkout authorizes collection, and
5
+ * an authenticated operator settles it only after money is actually received.
6
+ */
7
+ export const jarShopCodPaymentHandler = new PaymentMethodHandler({
8
+ code: "jarshop-cod-handler",
9
+ description: [
10
+ {
11
+ languageCode: LanguageCode.sk,
12
+ value: "Dobierka s manuálnym potvrdením prijatia platby",
13
+ },
14
+ {
15
+ languageCode: LanguageCode.en,
16
+ value: "Cash on delivery with manual payment collection",
17
+ },
18
+ ],
19
+ args: {},
20
+ createPayment: async (_ctx, order, amount, _args, metadata) => ({
21
+ amount,
22
+ state: "Authorized" as const,
23
+ transactionId: `cod:${order.code}`,
24
+ metadata,
25
+ }),
26
+ settlePayment: async () => ({ success: true }),
27
+ cancelPayment: async () => ({ success: true }),
28
+ });
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
 
3
3
  export const CONFIRMATION_TTL_MS = 2 * 60 * 60 * 1000;
4
+ export const CONFIRMATION_COOKIE_NAME = "jarshop-confirmation";
4
5
 
5
6
  export function hashConfirmationToken(token: string): string {
6
7
  return createHash("sha256").update(token).digest("hex");
@@ -15,12 +16,44 @@ export function createConfirmationToken(now = Date.now()) {
15
16
  };
16
17
  }
17
18
 
18
- export function canUseConfirmationToken(input: {
19
+ export function canExchangeConfirmationToken(input: {
19
20
  expiresAt: Date;
20
21
  consumedAt: Date | null;
21
- cookieMatches: boolean;
22
22
  now?: number;
23
23
  }): boolean {
24
- if (input.expiresAt.getTime() <= (input.now ?? Date.now())) return false;
25
- return input.consumedAt === null || input.cookieMatches;
24
+ return (
25
+ input.consumedAt === null &&
26
+ input.expiresAt.getTime() > (input.now ?? Date.now())
27
+ );
28
+ }
29
+
30
+ export function canReadConfirmationCredential(input: {
31
+ expiresAt: Date;
32
+ consumedAt: Date | null;
33
+ now?: number;
34
+ }): boolean {
35
+ return (
36
+ input.consumedAt !== null &&
37
+ input.expiresAt.getTime() > (input.now ?? Date.now())
38
+ );
39
+ }
40
+
41
+ export function readCookieValue(
42
+ cookieHeader: string,
43
+ cookieName: string,
44
+ ): string | undefined {
45
+ for (const part of cookieHeader.split(";")) {
46
+ const separator = part.indexOf("=");
47
+ if (separator < 0 || part.slice(0, separator).trim() !== cookieName) {
48
+ continue;
49
+ }
50
+ const value = part.slice(separator + 1).trim();
51
+ if (!value) return undefined;
52
+ try {
53
+ return decodeURIComponent(value);
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ return undefined;
26
59
  }
@@ -1,11 +1,15 @@
1
1
  export { CheckoutAttempt } from "./checkout-attempt.entity.js";
2
2
  export type { CheckoutAttemptStatus } from "./checkout-attempt.entity.js";
3
+ export { jarShopCodPaymentHandler } from "./cod-payment.js";
3
4
  export { JarShopConfirmationToken } from "./confirmation-token.entity.js";
4
5
  export {
5
6
  CONFIRMATION_TTL_MS,
6
- canUseConfirmationToken,
7
+ CONFIRMATION_COOKIE_NAME,
8
+ canExchangeConfirmationToken,
9
+ canReadConfirmationCredential,
7
10
  createConfirmationToken,
8
11
  hashConfirmationToken,
12
+ readCookieValue,
9
13
  } from "./confirmation.js";
10
14
  export { JarShopLegalConfiguration } from "./legal.entity.js";
11
15
  export { JarShopOrderEmailDelivery } from "./order-email-delivery.entity.js";
@@ -4,7 +4,7 @@ import { Inject, Injectable } from "@nestjs/common";
4
4
  import { Allow, Permission } from "@vendure/core";
5
5
  import { createHash } from "node:crypto";
6
6
  import { parse } from "graphql";
7
- import type { Repository } from "typeorm";
7
+ import { IsNull, MoreThan, type Repository } from "typeorm";
8
8
  import {
9
9
  ActiveOrderService,
10
10
  Ctx,
@@ -23,9 +23,12 @@ import {
23
23
  import { CheckoutAttempt } from "./checkout-attempt.entity.js";
24
24
  import { JarShopConfirmationToken } from "./confirmation-token.entity.js";
25
25
  import {
26
- canUseConfirmationToken,
26
+ CONFIRMATION_COOKIE_NAME,
27
+ canExchangeConfirmationToken,
28
+ canReadConfirmationCredential,
27
29
  createConfirmationToken,
28
30
  hashConfirmationToken,
31
+ readCookieValue,
29
32
  } from "./confirmation.js";
30
33
  import { JarShopLegalConfiguration } from "./legal.entity.js";
31
34
  import { JarShopOrderEmailDelivery } from "./order-email-delivery.entity.js";
@@ -111,6 +114,10 @@ const schema = parse(`
111
114
  totalWithTax: Int!
112
115
  currencyCode: String!
113
116
  }
117
+ type JarShopOrderConfirmationExchange {
118
+ credential: String!
119
+ confirmation: JarShopOrderConfirmation!
120
+ }
114
121
  input JarShopLegalConfigurationInput {
115
122
  languageCode: String!
116
123
  termsVersion: String!
@@ -120,10 +127,11 @@ const schema = parse(`
120
127
  }
121
128
  extend type Query {
122
129
  jarShopLegalConfiguration(languageCode: String!): JarShopLegalConfiguration
123
- jarShopOrderConfirmation(token: String!): JarShopOrderConfirmation
130
+ jarShopProtectedOrderConfirmation(orderCode: String!): JarShopOrderConfirmation
124
131
  }
125
132
  extend type Mutation {
126
133
  placeJarShopOrder(input: JarShopPlaceOrderInput!): JarShopPlaceOrderResult!
134
+ exchangeJarShopOrderConfirmation(token: String!): JarShopOrderConfirmationExchange
127
135
  updateJarShopLegalConfiguration(input: JarShopLegalConfigurationInput!): JarShopLegalConfiguration!
128
136
  }
129
137
  `);
@@ -222,8 +230,8 @@ class JarShopCheckoutResolver {
222
230
  return configuration ? toLegalConfiguration(configuration) : null;
223
231
  }
224
232
 
225
- @Query()
226
- async jarShopOrderConfirmation(
233
+ @Mutation()
234
+ async exchangeJarShopOrderConfirmation(
227
235
  @Ctx() ctx: RequestContext,
228
236
  @Args("token") token: string,
229
237
  ) {
@@ -234,31 +242,70 @@ class JarShopCheckoutResolver {
234
242
  const record = await repository.findOne({
235
243
  where: { tokenHash: hashConfirmationToken(token) },
236
244
  });
237
- if (!record || record.expiresAt.getTime() <= Date.now()) return null;
238
- const cookie = ctx.req?.headers.cookie ?? "";
239
- const exchanged = cookie.includes(`jarshop-confirmation=${token}`);
245
+ const now = new Date();
240
246
  if (
241
- !canUseConfirmationToken({
247
+ !record ||
248
+ !canExchangeConfirmationToken({
242
249
  expiresAt: record.expiresAt,
243
250
  consumedAt: record.consumedAt,
244
- cookieMatches: exchanged,
251
+ now: now.getTime(),
245
252
  })
246
253
  ) {
247
254
  return null;
248
255
  }
249
- if (!record.consumedAt) {
250
- record.consumedAt = new Date();
251
- await repository.save(record);
256
+ const order = await this.orderService.findOne(ctx, record.orderId);
257
+ if (!order) return null;
258
+
259
+ const credential = createConfirmationToken(now.getTime());
260
+ const update = await repository.update(
261
+ {
262
+ id: record.id,
263
+ tokenHash: record.tokenHash,
264
+ consumedAt: IsNull(),
265
+ expiresAt: MoreThan(now),
266
+ },
267
+ {
268
+ tokenHash: credential.tokenHash,
269
+ consumedAt: now,
270
+ },
271
+ );
272
+ if (update.affected !== 1) return null;
273
+
274
+ return {
275
+ credential: credential.token,
276
+ confirmation: toOrderConfirmation(order),
277
+ };
278
+ }
279
+
280
+ @Query()
281
+ async jarShopProtectedOrderConfirmation(
282
+ @Ctx() ctx: RequestContext,
283
+ @Args("orderCode") orderCode: string,
284
+ ) {
285
+ const credential = readCookieValue(
286
+ ctx.req?.headers.cookie ?? "",
287
+ CONFIRMATION_COOKIE_NAME,
288
+ );
289
+ if (!credential) return null;
290
+ const repository = this.connection.getRepository(
291
+ ctx,
292
+ JarShopConfirmationToken,
293
+ );
294
+ const record = await repository.findOne({
295
+ where: { tokenHash: hashConfirmationToken(credential) },
296
+ });
297
+ if (
298
+ !record ||
299
+ !canReadConfirmationCredential({
300
+ expiresAt: record.expiresAt,
301
+ consumedAt: record.consumedAt,
302
+ })
303
+ ) {
304
+ return null;
252
305
  }
253
306
  const order = await this.orderService.findOne(ctx, record.orderId);
254
- return order
255
- ? {
256
- orderId: order.id,
257
- orderCode: order.code,
258
- state: order.state,
259
- totalWithTax: order.totalWithTax,
260
- currencyCode: order.currencyCode,
261
- }
307
+ return order && order.code === orderCode
308
+ ? toOrderConfirmation(order)
262
309
  : null;
263
310
  }
264
311
 
@@ -536,8 +583,8 @@ class JarShopCheckoutResolver {
536
583
  ctx,
537
584
  activeOrder.id,
538
585
  {
539
- method: "dummy-payment-method",
540
- metadata: { attemptId: input.attemptId, method: "cod" },
586
+ method: "cod",
587
+ metadata: { attemptId: input.attemptId },
541
588
  },
542
589
  );
543
590
  if (isGraphQlErrorResult(paymentResult)) {
@@ -783,6 +830,16 @@ function toLegalConfiguration(configuration: JarShopLegalConfiguration) {
783
830
  };
784
831
  }
785
832
 
833
+ function toOrderConfirmation(order: Order) {
834
+ return {
835
+ orderId: order.id,
836
+ orderCode: order.code,
837
+ state: order.state,
838
+ totalWithTax: order.totalWithTax,
839
+ currencyCode: order.currencyCode,
840
+ };
841
+ }
842
+
786
843
  /**
787
844
  * JarShop's server-owned checkout seam.
788
845
  *
@@ -275,7 +275,7 @@ export function createOrderConfirmationUrl(
275
275
  const storefrontUrl =
276
276
  process.env.JARSHOP_STOREFRONT_URL ?? defaultStorefrontUrl;
277
277
  return new URL(
278
- `/${languageCode}/checkout/success?token=${encodeURIComponent(token)}`,
278
+ `/${languageCode}/checkout/confirmation?token=${encodeURIComponent(token)}`,
279
279
  storefrontUrl,
280
280
  ).toString();
281
281
  }
@@ -156,26 +156,48 @@ async function seed(): Promise<void> {
156
156
  channel.id,
157
157
  ]);
158
158
 
159
- const paymentMethods =
160
- await paymentMethodService.getActivePaymentMethods(ctx);
161
- const cod =
162
- paymentMethods.find((method) => method.code === "dummy-payment-method") ??
163
- (await paymentMethodService.create(ctx, {
164
- code: "dummy-payment-method",
165
- enabled: true,
166
- handler: {
167
- code: "dummy-payment-handler",
168
- arguments: [{ name: "automaticSettle", value: "false" }],
169
- },
170
- translations: [
171
- { languageCode: LanguageCode.sk, name: "Dobierka", description: "" },
172
- {
173
- languageCode: LanguageCode.en,
174
- name: "Cash on delivery",
175
- description: "",
159
+ const paymentMethods = await paymentMethodService.findAll(ctx);
160
+ const legacyDummy = paymentMethods.items.find(
161
+ (method) => method.code === "dummy-payment-method",
162
+ );
163
+ if (legacyDummy?.enabled) {
164
+ await paymentMethodService.update(ctx, {
165
+ id: legacyDummy.id,
166
+ enabled: false,
167
+ });
168
+ }
169
+ const existingCod = paymentMethods.items.find(
170
+ (method) => method.code === "cod",
171
+ );
172
+ const cod = existingCod
173
+ ? await paymentMethodService.update(ctx, {
174
+ id: existingCod.id,
175
+ enabled: true,
176
+ handler: {
177
+ code: "jarshop-cod-handler",
178
+ arguments: [],
176
179
  },
177
- ],
178
- }));
180
+ })
181
+ : await paymentMethodService.create(ctx, {
182
+ code: "cod",
183
+ enabled: true,
184
+ handler: {
185
+ code: "jarshop-cod-handler",
186
+ arguments: [],
187
+ },
188
+ translations: [
189
+ {
190
+ languageCode: LanguageCode.sk,
191
+ name: "Dobierka",
192
+ description: "",
193
+ },
194
+ {
195
+ languageCode: LanguageCode.en,
196
+ name: "Cash on delivery",
197
+ description: "",
198
+ },
199
+ ],
200
+ });
179
201
  await channelService.assignToChannels(ctx, PaymentMethod, cod.id, [
180
202
  channel.id,
181
203
  ]);
@@ -3,7 +3,6 @@ import {
3
3
  DefaultSchedulerPlugin,
4
4
  DefaultSearchPlugin,
5
5
  VendureConfig,
6
- dummyPaymentHandler,
7
6
  } from "@vendure/core";
8
7
  import { AssetServerPlugin } from "@vendure/asset-server-plugin";
9
8
  import { DashboardPlugin } from "@vendure/dashboard/plugin";
@@ -13,7 +12,10 @@ import {
13
12
  defaultEmailHandlers,
14
13
  } from "@vendure/email-plugin";
15
14
  import { GraphiqlPlugin } from "@vendure/graphiql-plugin";
16
- import { JarShopCheckoutPlugin } from "./plugins/jarshop-checkout/index.js";
15
+ import {
16
+ JarShopCheckoutPlugin,
17
+ jarShopCodPaymentHandler,
18
+ } from "./plugins/jarshop-checkout/index.js";
17
19
  import "dotenv/config";
18
20
  import path from "node:path";
19
21
 
@@ -76,7 +78,7 @@ export const config: VendureConfig = {
76
78
  username: process.env.DB_USERNAME,
77
79
  password: process.env.DB_PASSWORD,
78
80
  },
79
- paymentOptions: { paymentMethodHandlers: [dummyPaymentHandler] },
81
+ paymentOptions: { paymentMethodHandlers: [jarShopCodPaymentHandler] },
80
82
  plugins: [
81
83
  JarShopCheckoutPlugin,
82
84
  GraphiqlPlugin.init(),
@@ -0,0 +1,21 @@
1
+ import { NextResponse, type NextRequest } from "next/server";
2
+
3
+ import { exchangeOrderConfirmation } from "../../../../lib/confirmation";
4
+ import { parseLocale } from "../../../../lib/i18n";
5
+
6
+ export async function GET(
7
+ request: NextRequest,
8
+ context: { params: Promise<{ locale: string }> },
9
+ ) {
10
+ const { locale } = await context.params;
11
+ const language = parseLocale(locale);
12
+ const token = request.nextUrl.searchParams.get("token");
13
+ const confirmation = token
14
+ ? await exchangeOrderConfirmation(language, token)
15
+ : null;
16
+ const target = new URL(`/${language}/checkout/success`, request.url);
17
+ if (confirmation) {
18
+ target.searchParams.set("order", confirmation.orderCode);
19
+ }
20
+ return NextResponse.redirect(target);
21
+ }
@@ -3,15 +3,20 @@ import { getOrderConfirmation } from "../../../../lib/confirmation";
3
3
 
4
4
  interface CheckoutSuccessProps {
5
5
  params: Promise<{ locale: string }>;
6
+ searchParams: Promise<{ order?: string }>;
6
7
  }
7
8
 
8
9
  export default async function CheckoutSuccess({
9
10
  params,
11
+ searchParams,
10
12
  }: CheckoutSuccessProps) {
11
13
  const { locale } = await params;
14
+ const { order } = await searchParams;
12
15
  const language = parseLocale(locale);
13
16
  const t = createTranslator(language);
14
- const confirmation = await getOrderConfirmation(language);
17
+ const confirmation = order
18
+ ? await getOrderConfirmation(language, order)
19
+ : null;
15
20
  return (
16
21
  <main lang={language}>
17
22
  <h1>{t("success.title")}</h1>
@@ -17,11 +17,11 @@ export default async function LocalePage({ params }: LocalePageProps) {
17
17
  <p className="eyebrow">JarShop starter</p>
18
18
  <h1>{t("catalog.title")}</h1>
19
19
  </div>
20
- {!result.ok || result.data.length === 0 ? (
20
+ {!result.ok || result.data.items.length === 0 ? (
21
21
  <p>{t("catalog.empty")}</p>
22
22
  ) : (
23
23
  <ul className="product-grid">
24
- {result.data.map((product) => (
24
+ {result.data.items.map((product) => (
25
25
  <li key={product.id}>
26
26
  <a href={`/${language}/${product.slug}`}>{product.name}</a>
27
27
  <p>{product.description}</p>
@@ -5,5 +5,12 @@ import nextTypeScript from "eslint-config-next/typescript";
5
5
  export default defineConfig([
6
6
  ...nextVitals,
7
7
  ...nextTypeScript,
8
- globalIgnores([".next/**", "dist/**", "out/**", "build/**", "next-env.d.ts"]),
8
+ globalIgnores([
9
+ ".next/**",
10
+ "dist/**",
11
+ "out/**",
12
+ "build/**",
13
+ "src/gql/**",
14
+ "next-env.d.ts",
15
+ ]),
9
16
  ]);
@@ -8,11 +8,8 @@ import {
8
8
  commerceSessionAuthorization,
9
9
  persistCommerceSession,
10
10
  } from "./commerce-session";
11
- import {
12
- confirmationCookieOptions,
13
- CONFIRMATION_COOKIE_NAME,
14
- } from "./confirmation-cookie";
15
11
  import { PLACE_JARSHOP_ORDER_MUTATION } from "./checkout-contract";
12
+ import { exchangeOrderConfirmation } from "./confirmation";
16
13
 
17
14
  export interface CheckoutCartSnapshot {
18
15
  currencyCode: string;
@@ -133,11 +130,20 @@ export async function placeOrder(
133
130
  affectedLineIds: result?.affectedLineIds ?? [],
134
131
  };
135
132
  }
136
- store.set(
137
- CONFIRMATION_COOKIE_NAME,
133
+ const confirmation = await exchangeOrderConfirmation(
134
+ locale,
138
135
  result.confirmationToken,
139
- confirmationCookieOptions,
140
136
  );
141
- redirect(`/${locale}/checkout/success`);
137
+ if (!confirmation || confirmation.orderCode !== result.orderCode) {
138
+ return {
139
+ ok: false,
140
+ errorCode: "ORDER_NOT_FOUND",
141
+ message: "Order confirmation is not available",
142
+ affectedLineIds: [],
143
+ };
144
+ }
145
+ redirect(
146
+ `/${locale}/checkout/success?order=${encodeURIComponent(result.orderCode)}`,
147
+ );
142
148
  return { ok: true };
143
149
  }
@@ -6,9 +6,12 @@ import type {
6
6
  AccountAddressInput,
7
7
  AccountProfile,
8
8
  AccountOrdersOptions,
9
+ CatalogPage,
9
10
  ChangePasswordInput,
10
11
  CommerceResult,
11
12
  LoginInput,
13
+ ProductDetail,
14
+ ProductListOptions,
12
15
  ProductSummary,
13
16
  RegisterAccountInput,
14
17
  ResetPasswordInput,
@@ -37,14 +40,15 @@ function client(locale: CommerceLocale, sessionStore?: SessionStore) {
37
40
 
38
41
  export function getCatalog(
39
42
  locale: CommerceLocale,
40
- ): Promise<CommerceResult<ProductSummary[]>> {
41
- return client(locale).listProducts();
43
+ options?: ProductListOptions,
44
+ ): Promise<CommerceResult<CatalogPage<ProductSummary>>> {
45
+ return client(locale).listProducts(options);
42
46
  }
43
47
 
44
48
  export function getProduct(
45
49
  slug: string,
46
50
  locale: CommerceLocale,
47
- ): Promise<CommerceResult<ProductSummary | null>> {
51
+ ): Promise<CommerceResult<ProductDetail | null>> {
48
52
  return client(locale).getProductBySlug(slug);
49
53
  }
50
54