@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.
- package/README.md +2 -2
- package/dist/generator.js +4 -1
- package/dist/generator.js.map +1 -1
- package/dist/template/AGENTS.md +3 -3
- package/dist/template/CONTEXT.md +4 -0
- package/dist/template/README.md +5 -0
- package/dist/template/package.json +6 -1
- package/dist/template/pnpm-lock.yaml +2339 -79
- package/dist/template/pnpm-workspace.yaml +12 -1
- package/dist/template/src/plugins/jarshop-checkout/cod-payment.test.ts +44 -0
- package/dist/template/src/plugins/jarshop-checkout/cod-payment.ts +28 -0
- package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +37 -4
- package/dist/template/src/plugins/jarshop-checkout/index.ts +5 -1
- package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +80 -23
- package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +1 -1
- package/dist/template/src/seed.ts +41 -19
- package/dist/template/src/vendure-config.ts +5 -3
- package/dist/template/storefront/app/[locale]/checkout/confirmation/route.ts +21 -0
- package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +6 -1
- package/dist/template/storefront/app/[locale]/page.tsx +2 -2
- package/dist/template/storefront/eslint.config.mjs +8 -1
- package/dist/template/storefront/lib/checkout.ts +14 -8
- package/dist/template/storefront/lib/commerce.ts +7 -3
- package/dist/template/storefront/lib/confirmation.ts +66 -18
- package/dist/template/storefront/package.json +7 -3
- package/dist/template/storefront/vitest.config.ts +8 -0
- package/dist/template/turbo.json +7 -0
- package/dist/template-manifest.json +1 -1
- package/package.json +1 -1
- package/dist/template/storefront/pnpm-lock.yaml +0 -4316
- 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
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
226
|
-
async
|
|
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
|
-
|
|
238
|
-
const cookie = ctx.req?.headers.cookie ?? "";
|
|
239
|
-
const exchanged = cookie.includes(`jarshop-confirmation=${token}`);
|
|
245
|
+
const now = new Date();
|
|
240
246
|
if (
|
|
241
|
-
!
|
|
247
|
+
!record ||
|
|
248
|
+
!canExchangeConfirmationToken({
|
|
242
249
|
expiresAt: record.expiresAt,
|
|
243
250
|
consumedAt: record.consumedAt,
|
|
244
|
-
|
|
251
|
+
now: now.getTime(),
|
|
245
252
|
})
|
|
246
253
|
) {
|
|
247
254
|
return null;
|
|
248
255
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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: "
|
|
540
|
-
metadata: { attemptId: input.attemptId
|
|
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/
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 {
|
|
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: [
|
|
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 =
|
|
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([
|
|
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
|
-
|
|
137
|
-
|
|
133
|
+
const confirmation = await exchangeOrderConfirmation(
|
|
134
|
+
locale,
|
|
138
135
|
result.confirmationToken,
|
|
139
|
-
confirmationCookieOptions,
|
|
140
136
|
);
|
|
141
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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<
|
|
51
|
+
): Promise<CommerceResult<ProductDetail | null>> {
|
|
48
52
|
return client(locale).getProductBySlug(slug);
|
|
49
53
|
}
|
|
50
54
|
|