@jarwizz/create-jarshop 0.1.2 → 0.1.4
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 +50 -4
- package/dist/template/src/plugins/jarshop-checkout/index.ts +5 -1
- package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +95 -31
- 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,57 @@ export function createConfirmationToken(now = Date.now()) {
|
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Independent one-time credentials for the browser hand-off and e-mail link.
|
|
21
|
+
*
|
|
22
|
+
* Checkout exchanges the browser token immediately. Reusing it in the outbox
|
|
23
|
+
* would make the e-mail link a replay before the customer receives it.
|
|
24
|
+
*/
|
|
25
|
+
export function createConfirmationTokenPair(now = Date.now()) {
|
|
26
|
+
return {
|
|
27
|
+
browser: createConfirmationToken(now),
|
|
28
|
+
email: createConfirmationToken(now),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function canExchangeConfirmationToken(input: {
|
|
33
|
+
expiresAt: Date;
|
|
34
|
+
consumedAt: Date | null;
|
|
35
|
+
now?: number;
|
|
36
|
+
}): boolean {
|
|
37
|
+
return (
|
|
38
|
+
input.consumedAt === null &&
|
|
39
|
+
input.expiresAt.getTime() > (input.now ?? Date.now())
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function canReadConfirmationCredential(input: {
|
|
19
44
|
expiresAt: Date;
|
|
20
45
|
consumedAt: Date | null;
|
|
21
|
-
cookieMatches: boolean;
|
|
22
46
|
now?: number;
|
|
23
47
|
}): boolean {
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
return (
|
|
49
|
+
input.consumedAt !== null &&
|
|
50
|
+
input.expiresAt.getTime() > (input.now ?? Date.now())
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function readCookieValue(
|
|
55
|
+
cookieHeader: string,
|
|
56
|
+
cookieName: string,
|
|
57
|
+
): string | undefined {
|
|
58
|
+
for (const part of cookieHeader.split(";")) {
|
|
59
|
+
const separator = part.indexOf("=");
|
|
60
|
+
if (separator < 0 || part.slice(0, separator).trim() !== cookieName) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const value = part.slice(separator + 1).trim();
|
|
64
|
+
if (!value) return undefined;
|
|
65
|
+
try {
|
|
66
|
+
return decodeURIComponent(value);
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
26
72
|
}
|
|
@@ -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,13 @@ 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,
|
|
30
|
+
createConfirmationTokenPair,
|
|
28
31
|
hashConfirmationToken,
|
|
32
|
+
readCookieValue,
|
|
29
33
|
} from "./confirmation.js";
|
|
30
34
|
import { JarShopLegalConfiguration } from "./legal.entity.js";
|
|
31
35
|
import { JarShopOrderEmailDelivery } from "./order-email-delivery.entity.js";
|
|
@@ -111,6 +115,10 @@ const schema = parse(`
|
|
|
111
115
|
totalWithTax: Int!
|
|
112
116
|
currencyCode: String!
|
|
113
117
|
}
|
|
118
|
+
type JarShopOrderConfirmationExchange {
|
|
119
|
+
credential: String!
|
|
120
|
+
confirmation: JarShopOrderConfirmation!
|
|
121
|
+
}
|
|
114
122
|
input JarShopLegalConfigurationInput {
|
|
115
123
|
languageCode: String!
|
|
116
124
|
termsVersion: String!
|
|
@@ -120,10 +128,11 @@ const schema = parse(`
|
|
|
120
128
|
}
|
|
121
129
|
extend type Query {
|
|
122
130
|
jarShopLegalConfiguration(languageCode: String!): JarShopLegalConfiguration
|
|
123
|
-
|
|
131
|
+
jarShopProtectedOrderConfirmation(orderCode: String!): JarShopOrderConfirmation
|
|
124
132
|
}
|
|
125
133
|
extend type Mutation {
|
|
126
134
|
placeJarShopOrder(input: JarShopPlaceOrderInput!): JarShopPlaceOrderResult!
|
|
135
|
+
exchangeJarShopOrderConfirmation(token: String!): JarShopOrderConfirmationExchange
|
|
127
136
|
updateJarShopLegalConfiguration(input: JarShopLegalConfigurationInput!): JarShopLegalConfiguration!
|
|
128
137
|
}
|
|
129
138
|
`);
|
|
@@ -222,8 +231,8 @@ class JarShopCheckoutResolver {
|
|
|
222
231
|
return configuration ? toLegalConfiguration(configuration) : null;
|
|
223
232
|
}
|
|
224
233
|
|
|
225
|
-
@
|
|
226
|
-
async
|
|
234
|
+
@Mutation()
|
|
235
|
+
async exchangeJarShopOrderConfirmation(
|
|
227
236
|
@Ctx() ctx: RequestContext,
|
|
228
237
|
@Args("token") token: string,
|
|
229
238
|
) {
|
|
@@ -234,31 +243,70 @@ class JarShopCheckoutResolver {
|
|
|
234
243
|
const record = await repository.findOne({
|
|
235
244
|
where: { tokenHash: hashConfirmationToken(token) },
|
|
236
245
|
});
|
|
237
|
-
|
|
238
|
-
const cookie = ctx.req?.headers.cookie ?? "";
|
|
239
|
-
const exchanged = cookie.includes(`jarshop-confirmation=${token}`);
|
|
246
|
+
const now = new Date();
|
|
240
247
|
if (
|
|
241
|
-
!
|
|
248
|
+
!record ||
|
|
249
|
+
!canExchangeConfirmationToken({
|
|
242
250
|
expiresAt: record.expiresAt,
|
|
243
251
|
consumedAt: record.consumedAt,
|
|
244
|
-
|
|
252
|
+
now: now.getTime(),
|
|
245
253
|
})
|
|
246
254
|
) {
|
|
247
255
|
return null;
|
|
248
256
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
const order = await this.orderService.findOne(ctx, record.orderId);
|
|
258
|
+
if (!order) return null;
|
|
259
|
+
|
|
260
|
+
const credential = createConfirmationToken(now.getTime());
|
|
261
|
+
const update = await repository.update(
|
|
262
|
+
{
|
|
263
|
+
id: record.id,
|
|
264
|
+
tokenHash: record.tokenHash,
|
|
265
|
+
consumedAt: IsNull(),
|
|
266
|
+
expiresAt: MoreThan(now),
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
tokenHash: credential.tokenHash,
|
|
270
|
+
consumedAt: now,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
if (update.affected !== 1) return null;
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
credential: credential.token,
|
|
277
|
+
confirmation: toOrderConfirmation(order),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@Query()
|
|
282
|
+
async jarShopProtectedOrderConfirmation(
|
|
283
|
+
@Ctx() ctx: RequestContext,
|
|
284
|
+
@Args("orderCode") orderCode: string,
|
|
285
|
+
) {
|
|
286
|
+
const credential = readCookieValue(
|
|
287
|
+
ctx.req?.headers.cookie ?? "",
|
|
288
|
+
CONFIRMATION_COOKIE_NAME,
|
|
289
|
+
);
|
|
290
|
+
if (!credential) return null;
|
|
291
|
+
const repository = this.connection.getRepository(
|
|
292
|
+
ctx,
|
|
293
|
+
JarShopConfirmationToken,
|
|
294
|
+
);
|
|
295
|
+
const record = await repository.findOne({
|
|
296
|
+
where: { tokenHash: hashConfirmationToken(credential) },
|
|
297
|
+
});
|
|
298
|
+
if (
|
|
299
|
+
!record ||
|
|
300
|
+
!canReadConfirmationCredential({
|
|
301
|
+
expiresAt: record.expiresAt,
|
|
302
|
+
consumedAt: record.consumedAt,
|
|
303
|
+
})
|
|
304
|
+
) {
|
|
305
|
+
return null;
|
|
252
306
|
}
|
|
253
307
|
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
|
-
}
|
|
308
|
+
return order && order.code === orderCode
|
|
309
|
+
? toOrderConfirmation(order)
|
|
262
310
|
: null;
|
|
263
311
|
}
|
|
264
312
|
|
|
@@ -536,8 +584,8 @@ class JarShopCheckoutResolver {
|
|
|
536
584
|
ctx,
|
|
537
585
|
activeOrder.id,
|
|
538
586
|
{
|
|
539
|
-
method: "
|
|
540
|
-
metadata: { attemptId: input.attemptId
|
|
587
|
+
method: "cod",
|
|
588
|
+
metadata: { attemptId: input.attemptId },
|
|
541
589
|
},
|
|
542
590
|
);
|
|
543
591
|
if (isGraphQlErrorResult(paymentResult)) {
|
|
@@ -566,19 +614,25 @@ class JarShopCheckoutResolver {
|
|
|
566
614
|
acceptedAt: new Date(),
|
|
567
615
|
}),
|
|
568
616
|
);
|
|
569
|
-
const
|
|
617
|
+
const confirmations = createConfirmationTokenPair();
|
|
570
618
|
const tokenRepository = this.connection.getRepository(
|
|
571
619
|
ctx,
|
|
572
620
|
JarShopConfirmationToken,
|
|
573
621
|
);
|
|
574
|
-
await tokenRepository.save(
|
|
622
|
+
await tokenRepository.save([
|
|
575
623
|
tokenRepository.create({
|
|
576
|
-
tokenHash:
|
|
624
|
+
tokenHash: confirmations.browser.tokenHash,
|
|
577
625
|
orderId: Number(paymentResult.id),
|
|
578
|
-
expiresAt:
|
|
626
|
+
expiresAt: confirmations.browser.expiresAt,
|
|
579
627
|
consumedAt: null,
|
|
580
628
|
}),
|
|
581
|
-
|
|
629
|
+
tokenRepository.create({
|
|
630
|
+
tokenHash: confirmations.email.tokenHash,
|
|
631
|
+
orderId: Number(paymentResult.id),
|
|
632
|
+
expiresAt: confirmations.email.expiresAt,
|
|
633
|
+
consumedAt: null,
|
|
634
|
+
}),
|
|
635
|
+
]);
|
|
582
636
|
const emailDeliveryRepository = this.connection.getRepository(
|
|
583
637
|
ctx,
|
|
584
638
|
JarShopOrderEmailDelivery,
|
|
@@ -589,7 +643,7 @@ class JarShopCheckoutResolver {
|
|
|
589
643
|
orderId: Number(paymentResult.id),
|
|
590
644
|
checkoutAttemptId: attempt.attemptId,
|
|
591
645
|
languageCode: input.languageCode,
|
|
592
|
-
confirmationToken:
|
|
646
|
+
confirmationToken: confirmations.email.token,
|
|
593
647
|
}),
|
|
594
648
|
),
|
|
595
649
|
);
|
|
@@ -603,13 +657,13 @@ class JarShopCheckoutResolver {
|
|
|
603
657
|
placedOrder,
|
|
604
658
|
activeOrder.totalWithTax,
|
|
605
659
|
activeOrder.currencyCode,
|
|
606
|
-
|
|
660
|
+
confirmations.browser.token,
|
|
607
661
|
)
|
|
608
662
|
: success(
|
|
609
663
|
paymentResult,
|
|
610
664
|
activeOrder.totalWithTax,
|
|
611
665
|
activeOrder.currencyCode,
|
|
612
|
-
|
|
666
|
+
confirmations.browser.token,
|
|
613
667
|
);
|
|
614
668
|
}
|
|
615
669
|
|
|
@@ -783,6 +837,16 @@ function toLegalConfiguration(configuration: JarShopLegalConfiguration) {
|
|
|
783
837
|
};
|
|
784
838
|
}
|
|
785
839
|
|
|
840
|
+
function toOrderConfirmation(order: Order) {
|
|
841
|
+
return {
|
|
842
|
+
orderId: order.id,
|
|
843
|
+
orderCode: order.code,
|
|
844
|
+
state: order.state,
|
|
845
|
+
totalWithTax: order.totalWithTax,
|
|
846
|
+
currencyCode: order.currencyCode,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
786
850
|
/**
|
|
787
851
|
* JarShop's server-owned checkout seam.
|
|
788
852
|
*
|
|
@@ -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
|
|