@jarwizz/create-jarshop 0.1.1 → 0.1.2

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 (100) hide show
  1. package/README.md +8 -0
  2. package/dist/generator.js +6 -3
  3. package/dist/generator.js.map +1 -1
  4. package/dist/template/CONTEXT.md +10 -0
  5. package/dist/template/README.md +20 -3
  6. package/dist/template/docker-compose.yml +5 -0
  7. package/dist/template/docs/smtp-smoke.md +37 -0
  8. package/dist/template/env.example.template +18 -0
  9. package/dist/template/gitignore.template +2 -0
  10. package/dist/template/package.json +13 -5
  11. package/dist/template/pnpm-lock.yaml +38 -7
  12. package/dist/template/src/dev-storefront.ts +58 -0
  13. package/dist/template/src/dev.ts +67 -4
  14. package/dist/template/src/email-smoke.ts +27 -0
  15. package/dist/template/src/environment.d.ts +6 -0
  16. package/dist/template/src/migrate.ts +16 -0
  17. package/dist/template/src/migrations/1786024108434-jarshop-checkout-attempt.ts +28 -0
  18. package/dist/template/src/migrations/1786024108435-jarshop-legal-and-confirmation.ts +60 -0
  19. package/dist/template/src/migrations/1786024108436-jarshop-order-email-outbox.ts +37 -0
  20. package/dist/template/src/migrations/1786030000000-jarshop-order-email-delivery-lease.ts +27 -0
  21. package/dist/template/src/plugins/jarshop-checkout/checkout-attempt.entity.ts +42 -0
  22. package/dist/template/src/plugins/jarshop-checkout/confirmation-token.entity.ts +31 -0
  23. package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +26 -0
  24. package/dist/template/src/plugins/jarshop-checkout/dashboard/index.tsx +107 -0
  25. package/dist/template/src/plugins/jarshop-checkout/dashboard/order-email-delivery-status.ts +43 -0
  26. package/dist/template/src/plugins/jarshop-checkout/email-smoke-core.ts +56 -0
  27. package/dist/template/src/plugins/jarshop-checkout/index.ts +47 -0
  28. package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +813 -0
  29. package/dist/template/src/plugins/jarshop-checkout/legal.entity.ts +39 -0
  30. package/dist/template/src/plugins/jarshop-checkout/order-consent.entity.ts +32 -0
  31. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +337 -0
  32. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-status.ts +39 -0
  33. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery.entity.ts +66 -0
  34. package/dist/template/src/plugins/jarshop-checkout/order-email-outbox.ts +108 -0
  35. package/dist/template/src/plugins/jarshop-checkout/order-email-transport.ts +35 -0
  36. package/dist/template/src/plugins/jarshop-checkout/order-email-worker.ts +281 -0
  37. package/dist/template/src/plugins/jarshop-checkout/shipping.test.ts +41 -0
  38. package/dist/template/src/plugins/jarshop-checkout/shipping.ts +162 -0
  39. package/dist/template/src/seed.ts +149 -0
  40. package/dist/template/src/vendure-config.ts +68 -5
  41. package/dist/template/static/email/templates/email-verification/body.hbs +4 -4
  42. package/dist/template/storefront/app/[locale]/[slug]/add-to-cart-form.tsx +44 -0
  43. package/dist/template/storefront/app/[locale]/[slug]/page.tsx +35 -26
  44. package/dist/template/storefront/app/[locale]/account/account-mutation-form.tsx +44 -0
  45. package/dist/template/storefront/app/[locale]/account/change-password-form.tsx +65 -0
  46. package/dist/template/storefront/app/[locale]/account/login/login-form.tsx +60 -0
  47. package/dist/template/storefront/app/[locale]/account/login/page.tsx +23 -0
  48. package/dist/template/storefront/app/[locale]/account/page.tsx +215 -0
  49. package/dist/template/storefront/app/[locale]/account/password-reset/callback/route.ts +33 -0
  50. package/dist/template/storefront/app/[locale]/account/password-reset/complete/page.tsx +27 -0
  51. package/dist/template/storefront/app/[locale]/account/password-reset/page.tsx +37 -0
  52. package/dist/template/storefront/app/[locale]/account/password-reset/password-reset-forms.tsx +99 -0
  53. package/dist/template/storefront/app/[locale]/account/register/page.tsx +23 -0
  54. package/dist/template/storefront/app/[locale]/account/register/register-form.tsx +67 -0
  55. package/dist/template/storefront/app/[locale]/account/verify/complete/route.ts +33 -0
  56. package/dist/template/storefront/app/[locale]/account/verify/page.tsx +41 -0
  57. package/dist/template/storefront/app/[locale]/account/verify/refresh-form.tsx +35 -0
  58. package/dist/template/storefront/app/[locale]/cart/page.tsx +78 -0
  59. package/dist/template/storefront/app/[locale]/checkout/checkout-form.tsx +112 -0
  60. package/dist/template/storefront/app/[locale]/checkout/page.tsx +44 -0
  61. package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +23 -0
  62. package/dist/template/storefront/app/[locale]/layout.tsx +48 -0
  63. package/dist/template/storefront/app/[locale]/page.tsx +10 -11
  64. package/dist/template/storefront/app/globals.css +372 -4
  65. package/dist/template/storefront/app/layout.tsx +13 -0
  66. package/dist/template/storefront/eslint.config.mjs +1 -1
  67. package/dist/template/storefront/gitignore.template +1 -0
  68. package/dist/template/storefront/lib/account-security.test.ts +74 -0
  69. package/dist/template/storefront/lib/account-security.ts +81 -0
  70. package/dist/template/storefront/lib/account.ts +366 -0
  71. package/dist/template/storefront/lib/cart-contract.ts +52 -0
  72. package/dist/template/storefront/lib/cart.test.ts +30 -0
  73. package/dist/template/storefront/lib/cart.ts +130 -0
  74. package/dist/template/storefront/lib/checkout-contract.test.ts +20 -0
  75. package/dist/template/storefront/lib/checkout-contract.ts +16 -0
  76. package/dist/template/storefront/lib/checkout.ts +143 -0
  77. package/dist/template/storefront/lib/commerce-session.test.ts +77 -0
  78. package/dist/template/storefront/lib/commerce-session.ts +85 -0
  79. package/dist/template/storefront/lib/commerce.ts +122 -1
  80. package/dist/template/storefront/lib/confirmation-cookie.ts +9 -0
  81. package/dist/template/storefront/lib/confirmation.ts +47 -0
  82. package/dist/template/storefront/lib/form-values.test.ts +14 -0
  83. package/dist/template/storefront/lib/form-values.ts +7 -0
  84. package/dist/template/storefront/lib/i18n.test.ts +50 -0
  85. package/dist/template/storefront/lib/i18n.ts +44 -0
  86. package/dist/template/storefront/lib/password-reset-token.test.ts +31 -0
  87. package/dist/template/storefront/lib/password-reset-token.ts +17 -0
  88. package/dist/template/storefront/lib/valkey-rate-limit.ts +72 -0
  89. package/dist/template/storefront/messages/en.json +89 -0
  90. package/dist/template/storefront/messages/sk.json +89 -0
  91. package/dist/template/storefront/next.config.ts +3 -1
  92. package/dist/template/storefront/package.json +4 -1
  93. package/dist/template/storefront/pnpm-lock.yaml +756 -2
  94. package/dist/template/tsconfig.json +2 -1
  95. package/dist/template/turbo.json +1 -0
  96. package/dist/template/vite.config.mts +4 -1
  97. package/dist/template-manifest.json +1 -1
  98. package/dist/template.js +3 -0
  99. package/dist/template.js.map +1 -1
  100. package/package.json +1 -1
@@ -0,0 +1,143 @@
1
+ "use server";
2
+
3
+ import { cookies } from "next/headers";
4
+ import { redirect } from "next/navigation";
5
+
6
+ import type { CommerceLocale } from "./commerce";
7
+ import {
8
+ commerceSessionAuthorization,
9
+ persistCommerceSession,
10
+ } from "./commerce-session";
11
+ import {
12
+ confirmationCookieOptions,
13
+ CONFIRMATION_COOKIE_NAME,
14
+ } from "./confirmation-cookie";
15
+ import { PLACE_JARSHOP_ORDER_MUTATION } from "./checkout-contract";
16
+
17
+ export interface CheckoutCartSnapshot {
18
+ currencyCode: string;
19
+ subtotalWithTax: number;
20
+ lines: Array<{
21
+ lineId: string;
22
+ quantity: number;
23
+ linePriceWithTax: number;
24
+ }>;
25
+ }
26
+
27
+ export type CheckoutShippingMethod = "pickup" | "courier";
28
+
29
+ type CheckoutResponse = {
30
+ data?: {
31
+ placeJarShopOrder: {
32
+ success: boolean;
33
+ orderCode: string | null;
34
+ errorCode: string | null;
35
+ message: string | null;
36
+ affectedLineIds: string[];
37
+ confirmationToken: string | null;
38
+ };
39
+ };
40
+ errors?: Array<{ message: string }>;
41
+ };
42
+
43
+ export type CheckoutActionState =
44
+ | { ok: true }
45
+ | {
46
+ ok: false;
47
+ errorCode: string;
48
+ message: string;
49
+ affectedLineIds: string[];
50
+ };
51
+
52
+ export async function placeOrder(
53
+ locale: CommerceLocale,
54
+ attemptId: string,
55
+ cartSnapshot: CheckoutCartSnapshot | null,
56
+ _previousState: CheckoutActionState,
57
+ formData: FormData,
58
+ ): Promise<CheckoutActionState> {
59
+ const store = await cookies();
60
+ const authorization = commerceSessionAuthorization(store);
61
+ const endpoint = new URL(
62
+ process.env.JARSHOP_COMMERCE_ENDPOINT ?? "http://localhost:3000/shop-api",
63
+ );
64
+ endpoint.searchParams.set("languageCode", locale);
65
+ const response = await fetch(endpoint, {
66
+ method: "POST",
67
+ headers: {
68
+ "content-type": "application/json",
69
+ ...(process.env.JARSHOP_CHANNEL_TOKEN
70
+ ? { "vendure-token": process.env.JARSHOP_CHANNEL_TOKEN }
71
+ : {}),
72
+ ...(authorization ? { authorization } : {}),
73
+ },
74
+ body: JSON.stringify({
75
+ query: PLACE_JARSHOP_ORDER_MUTATION,
76
+ variables: {
77
+ input: {
78
+ attemptId,
79
+ marketCode: "sk",
80
+ languageCode: locale,
81
+ currencyCode: "EUR",
82
+ customer: {
83
+ emailAddress: String(formData.get("emailAddress") ?? ""),
84
+ firstName: String(formData.get("firstName") ?? ""),
85
+ lastName: String(formData.get("lastName") ?? ""),
86
+ phoneNumber: String(formData.get("phoneNumber") ?? ""),
87
+ },
88
+ shippingMethodCode: String(
89
+ formData.get("shippingMethodCode") ?? "pickup",
90
+ ),
91
+ paymentMethodCode: "cod",
92
+ termsAccepted: formData.get("termsAccepted") === "on",
93
+ ...(cartSnapshot ? { cartSnapshot } : {}),
94
+ ...(formData.get("shippingMethodCode") === "courier"
95
+ ? {
96
+ shippingAddress: {
97
+ streetLine1: String(formData.get("streetLine1") ?? ""),
98
+ city: String(formData.get("city") ?? ""),
99
+ postalCode: String(formData.get("postalCode") ?? ""),
100
+ countryCode: String(formData.get("countryCode") ?? ""),
101
+ },
102
+ }
103
+ : {}),
104
+ },
105
+ },
106
+ }),
107
+ cache: "no-store",
108
+ });
109
+ persistCommerceSession(store, response.headers);
110
+ if (!response.ok) {
111
+ return {
112
+ ok: false,
113
+ errorCode: "TEMPORARY_FAILURE",
114
+ message: `Checkout API returned ${response.status}`,
115
+ affectedLineIds: [],
116
+ };
117
+ }
118
+ const body = (await response.json()) as CheckoutResponse;
119
+ if (body.errors?.length) {
120
+ return {
121
+ ok: false,
122
+ errorCode: "TEMPORARY_FAILURE",
123
+ message: body.errors[0]?.message ?? "Checkout failed",
124
+ affectedLineIds: [],
125
+ };
126
+ }
127
+ const result = body.data?.placeJarShopOrder;
128
+ if (!result?.success || !result.orderCode || !result.confirmationToken) {
129
+ return {
130
+ ok: false,
131
+ errorCode: result?.errorCode ?? "TEMPORARY_FAILURE",
132
+ message: result?.message ?? "Order could not be placed",
133
+ affectedLineIds: result?.affectedLineIds ?? [],
134
+ };
135
+ }
136
+ store.set(
137
+ CONFIRMATION_COOKIE_NAME,
138
+ result.confirmationToken,
139
+ confirmationCookieOptions,
140
+ );
141
+ redirect(`/${locale}/checkout/success`);
142
+ return { ok: true };
143
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ COMMERCE_SESSION_COOKIE,
5
+ type CommerceSessionStore,
6
+ createCommerceSessionStore,
7
+ commerceSessionAuthorization,
8
+ commerceSessionToken,
9
+ persistCommerceSession,
10
+ VENDURE_AUTH_TOKEN_HEADER,
11
+ } from "./commerce-session";
12
+
13
+ function createStore() {
14
+ const values = new Map<string, string>();
15
+ const options = new Map<string, Parameters<CommerceSessionStore["set"]>[2]>();
16
+ return {
17
+ get(name: string) {
18
+ const value = values.get(name);
19
+ return value === undefined ? undefined : { value };
20
+ },
21
+ set(
22
+ name: string,
23
+ value: string,
24
+ cookieOptions: Parameters<CommerceSessionStore["set"]>[2],
25
+ ) {
26
+ values.set(name, value);
27
+ options.set(name, cookieOptions);
28
+ },
29
+ delete(name: string) {
30
+ values.delete(name);
31
+ options.delete(name);
32
+ },
33
+ options,
34
+ };
35
+ }
36
+
37
+ describe("commerce session adapter", () => {
38
+ it("persists a Vendure response token and forwards it as bearer auth", () => {
39
+ const store = createStore();
40
+ const headers = new Headers();
41
+ headers.set(VENDURE_AUTH_TOKEN_HEADER, "session-value");
42
+
43
+ persistCommerceSession(store, headers);
44
+
45
+ expect(commerceSessionToken(store)).toBe("session-value");
46
+ expect(commerceSessionAuthorization(store)).toBe("Bearer session-value");
47
+ expect(store.get(COMMERCE_SESSION_COOKIE)).toEqual({
48
+ value: "session-value",
49
+ });
50
+ expect(store.options.get(COMMERCE_SESSION_COOKIE)).toMatchObject({
51
+ httpOnly: true,
52
+ maxAge: 60 * 60 * 24 * 30,
53
+ path: "/",
54
+ sameSite: "lax",
55
+ });
56
+ });
57
+
58
+ it("does not create a storefront session without a Shop API token", () => {
59
+ const store = createStore();
60
+
61
+ persistCommerceSession(store, new Headers());
62
+
63
+ expect(commerceSessionToken(store)).toBeUndefined();
64
+ expect(commerceSessionAuthorization(store)).toBeUndefined();
65
+ });
66
+
67
+ it("adapts the SDK session store to an HttpOnly storefront cookie", () => {
68
+ const store = createStore();
69
+ const session = createCommerceSessionStore(store);
70
+
71
+ session.set("new-session");
72
+ expect(commerceSessionToken(store)).toBe("new-session");
73
+
74
+ session.set(undefined);
75
+ expect(commerceSessionToken(store)).toBeUndefined();
76
+ });
77
+ });
@@ -0,0 +1,85 @@
1
+ import type { SessionStore } from "@jarwizz/commerce-sdk/types";
2
+
3
+ export const COMMERCE_SESSION_COOKIE = "jarshop-storefront-session";
4
+ export const VENDURE_AUTH_TOKEN_HEADER = "vendure-auth-token";
5
+
6
+ interface CookieValue {
7
+ readonly value: string;
8
+ }
9
+
10
+ export interface CommerceSessionStore {
11
+ get(name: string): CookieValue | undefined;
12
+ set(
13
+ name: string,
14
+ value: string,
15
+ options: {
16
+ httpOnly: boolean;
17
+ maxAge: number;
18
+ path: string;
19
+ sameSite: "lax";
20
+ secure: boolean;
21
+ },
22
+ ): void;
23
+ delete(name: string): void;
24
+ }
25
+
26
+ const storefrontCookieOptions = {
27
+ httpOnly: true,
28
+ maxAge: 60 * 60 * 24 * 30,
29
+ path: "/",
30
+ sameSite: "lax" as const,
31
+ secure: process.env.NODE_ENV === "production",
32
+ };
33
+
34
+ /**
35
+ * Reads the opaque Shop API session only in a server-side facade call.
36
+ * Browser JavaScript cannot access the HttpOnly cookie.
37
+ */
38
+ export function commerceSessionToken(
39
+ store: Pick<CommerceSessionStore, "get">,
40
+ ): string | undefined {
41
+ return store.get(COMMERCE_SESSION_COOKIE)?.value;
42
+ }
43
+
44
+ /**
45
+ * Vendure returns a newly-created session in `vendure-auth-token`, but accepts
46
+ * a later bearer session only through the standard Authorization header.
47
+ */
48
+ export function commerceSessionAuthorization(
49
+ store: Pick<CommerceSessionStore, "get">,
50
+ ): string | undefined {
51
+ const token = commerceSessionToken(store);
52
+ return token ? `Bearer ${token}` : undefined;
53
+ }
54
+
55
+ /**
56
+ * Captures Vendure's bearer session from an internal API response and stores it
57
+ * under the storefront origin. The browser never receives a Shop API header.
58
+ */
59
+ export function persistCommerceSession(
60
+ store: Pick<CommerceSessionStore, "set">,
61
+ headers: Headers,
62
+ ): void {
63
+ const token = headers.get(VENDURE_AUTH_TOKEN_HEADER);
64
+ if (!token) return;
65
+ store.set(COMMERCE_SESSION_COOKIE, token, storefrontCookieOptions);
66
+ }
67
+
68
+ /**
69
+ * Binds the framework-neutral SDK session contract to the storefront's
70
+ * HttpOnly cookie. Browser code never receives the opaque token.
71
+ */
72
+ export function createCommerceSessionStore(
73
+ store: CommerceSessionStore,
74
+ ): SessionStore {
75
+ return {
76
+ get: () => commerceSessionToken(store),
77
+ set: (token) => {
78
+ if (token) {
79
+ store.set(COMMERCE_SESSION_COOKIE, token, storefrontCookieOptions);
80
+ } else {
81
+ store.delete(COMMERCE_SESSION_COOKIE);
82
+ }
83
+ },
84
+ };
85
+ }
@@ -1,7 +1,20 @@
1
1
  import { createCommerceClient } from "@jarwizz/commerce-sdk/server";
2
2
  import type {
3
+ AccountSession,
4
+ AccountSummary,
5
+ AccountAddress,
6
+ AccountAddressInput,
7
+ AccountProfile,
8
+ AccountOrdersOptions,
9
+ ChangePasswordInput,
3
10
  CommerceResult,
11
+ LoginInput,
4
12
  ProductSummary,
13
+ RegisterAccountInput,
14
+ ResetPasswordInput,
15
+ SessionStore,
16
+ UpdateAccountProfileInput,
17
+ UpdateAccountAddressInput,
5
18
  } from "@jarwizz/commerce-sdk/types";
6
19
 
7
20
  export type CommerceLocale = "sk" | "en";
@@ -14,10 +27,11 @@ function endpointFor(locale: CommerceLocale): string {
14
27
  return endpoint.toString();
15
28
  }
16
29
 
17
- function client(locale: CommerceLocale) {
30
+ function client(locale: CommerceLocale, sessionStore?: SessionStore) {
18
31
  return createCommerceClient({
19
32
  endpoint: endpointFor(locale),
20
33
  channelToken: process.env.JARSHOP_CHANNEL_TOKEN,
34
+ sessionStore,
21
35
  });
22
36
  }
23
37
 
@@ -33,3 +47,110 @@ export function getProduct(
33
47
  ): Promise<CommerceResult<ProductSummary | null>> {
34
48
  return client(locale).getProductBySlug(slug);
35
49
  }
50
+
51
+ export function loginAccount(
52
+ locale: CommerceLocale,
53
+ sessionStore: SessionStore,
54
+ input: LoginInput,
55
+ ): Promise<CommerceResult<AccountSession>> {
56
+ return client(locale, sessionStore).login(input);
57
+ }
58
+
59
+ export function logoutAccount(
60
+ locale: CommerceLocale,
61
+ sessionStore: SessionStore,
62
+ ): Promise<CommerceResult<void>> {
63
+ return client(locale, sessionStore).logout();
64
+ }
65
+
66
+ export function getActiveAccount(
67
+ locale: CommerceLocale,
68
+ sessionStore: SessionStore,
69
+ ): Promise<CommerceResult<AccountSummary | null>> {
70
+ return client(locale, sessionStore).getActiveAccount();
71
+ }
72
+
73
+ export function registerAccount(
74
+ locale: CommerceLocale,
75
+ input: RegisterAccountInput,
76
+ ): Promise<CommerceResult<void>> {
77
+ return client(locale).register(input);
78
+ }
79
+
80
+ export function refreshAccountRegistration(
81
+ locale: CommerceLocale,
82
+ emailAddress: string,
83
+ ): Promise<CommerceResult<void>> {
84
+ return client(locale).refreshRegistration(emailAddress);
85
+ }
86
+
87
+ export function verifyAccountRegistration(
88
+ locale: CommerceLocale,
89
+ sessionStore: SessionStore,
90
+ token: string,
91
+ ): Promise<CommerceResult<AccountSession>> {
92
+ return client(locale, sessionStore).verifyRegistration(token);
93
+ }
94
+
95
+ export function requestAccountPasswordReset(
96
+ locale: CommerceLocale,
97
+ emailAddress: string,
98
+ ): Promise<CommerceResult<void>> {
99
+ return client(locale).requestPasswordReset(emailAddress);
100
+ }
101
+
102
+ export function resetAccountPassword(
103
+ locale: CommerceLocale,
104
+ sessionStore: SessionStore,
105
+ input: ResetPasswordInput,
106
+ ): Promise<CommerceResult<AccountSession>> {
107
+ return client(locale, sessionStore).resetPassword(input);
108
+ }
109
+
110
+ export function getAccountProfile(
111
+ locale: CommerceLocale,
112
+ sessionStore: SessionStore,
113
+ options?: AccountOrdersOptions,
114
+ ): Promise<CommerceResult<AccountProfile | null>> {
115
+ return client(locale, sessionStore).getAccountProfile(options);
116
+ }
117
+
118
+ export function updateAccountProfile(
119
+ locale: CommerceLocale,
120
+ sessionStore: SessionStore,
121
+ input: UpdateAccountProfileInput,
122
+ ): Promise<CommerceResult<AccountSummary>> {
123
+ return client(locale, sessionStore).updateProfile(input);
124
+ }
125
+
126
+ export function createAccountAddress(
127
+ locale: CommerceLocale,
128
+ sessionStore: SessionStore,
129
+ input: AccountAddressInput,
130
+ ): Promise<CommerceResult<AccountAddress>> {
131
+ return client(locale, sessionStore).createAddress(input);
132
+ }
133
+
134
+ export function deleteAccountAddress(
135
+ locale: CommerceLocale,
136
+ sessionStore: SessionStore,
137
+ id: string,
138
+ ): Promise<CommerceResult<void>> {
139
+ return client(locale, sessionStore).deleteAddress(id);
140
+ }
141
+
142
+ export function updateAccountAddress(
143
+ locale: CommerceLocale,
144
+ sessionStore: SessionStore,
145
+ input: UpdateAccountAddressInput,
146
+ ): Promise<CommerceResult<AccountAddress>> {
147
+ return client(locale, sessionStore).updateAddress(input);
148
+ }
149
+
150
+ export function changeAccountPassword(
151
+ locale: CommerceLocale,
152
+ sessionStore: SessionStore,
153
+ input: ChangePasswordInput,
154
+ ): Promise<CommerceResult<void>> {
155
+ return client(locale, sessionStore).changePassword(input);
156
+ }
@@ -0,0 +1,9 @@
1
+ export const CONFIRMATION_COOKIE_NAME = "jarshop-confirmation";
2
+
3
+ export const confirmationCookieOptions = {
4
+ httpOnly: true,
5
+ secure: process.env.NODE_ENV === "production",
6
+ sameSite: "lax" as const,
7
+ path: "/",
8
+ maxAge: 60 * 60 * 2,
9
+ };
@@ -0,0 +1,47 @@
1
+ "use server";
2
+
3
+ import { cookies } from "next/headers";
4
+
5
+ import type { CommerceLocale } from "./commerce";
6
+ import { CONFIRMATION_COOKIE_NAME } from "./confirmation-cookie";
7
+
8
+ export interface OrderConfirmationView {
9
+ orderId: string;
10
+ orderCode: string;
11
+ state: string;
12
+ totalWithTax: number;
13
+ currencyCode: string;
14
+ }
15
+
16
+ export async function getOrderConfirmation(
17
+ locale: CommerceLocale,
18
+ ): Promise<OrderConfirmationView | null> {
19
+ const store = await cookies();
20
+ const token = store.get(CONFIRMATION_COOKIE_NAME)?.value;
21
+ if (!token) return null;
22
+ const endpoint = new URL(
23
+ process.env.JARSHOP_COMMERCE_ENDPOINT ?? "http://localhost:3000/shop-api",
24
+ );
25
+ endpoint.searchParams.set("languageCode", locale);
26
+ const response = await fetch(endpoint, {
27
+ method: "POST",
28
+ headers: {
29
+ "content-type": "application/json",
30
+ ...(process.env.JARSHOP_CHANNEL_TOKEN
31
+ ? { "vendure-token": process.env.JARSHOP_CHANNEL_TOKEN }
32
+ : {}),
33
+ cookie: `${CONFIRMATION_COOKIE_NAME}=${token}`,
34
+ },
35
+ body: JSON.stringify({
36
+ query: `query Confirmation($token: String!) { jarShopOrderConfirmation(token: $token) { orderId orderCode state totalWithTax currencyCode } }`,
37
+ variables: { token },
38
+ }),
39
+ cache: "no-store",
40
+ });
41
+ if (!response.ok) return null;
42
+ const body = (await response.json()) as {
43
+ data?: { jarShopOrderConfirmation: OrderConfirmationView | null };
44
+ errors?: Array<{ message: string }>;
45
+ };
46
+ return body.data?.jarShopOrderConfirmation ?? null;
47
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { formPassword, formText } from "./form-values";
4
+
5
+ describe("account form values", () => {
6
+ it("trims ordinary text but preserves password bytes", () => {
7
+ const form = new FormData();
8
+ form.set("name", " Ada ");
9
+ form.set("password", " correct horse ");
10
+
11
+ expect(formText(form, "name")).toBe("Ada");
12
+ expect(formPassword(form, "password")).toBe(" correct horse ");
13
+ });
14
+ });
@@ -0,0 +1,7 @@
1
+ export function formText(formData: FormData, name: string): string {
2
+ return String(formData.get(name) ?? "").trim();
3
+ }
4
+
5
+ export function formPassword(formData: FormData, name: string): string {
6
+ return String(formData.get(name) ?? "");
7
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ createTranslator,
4
+ formatMessage,
5
+ isSupportedLocale,
6
+ messages,
7
+ parseLocale,
8
+ supportedLocales,
9
+ } from "./i18n";
10
+
11
+ describe("JarShop storefront i18n contract", () => {
12
+ it("keeps every supported locale aligned with the typed message keys", () => {
13
+ const keys = Object.keys(messages.sk).sort();
14
+
15
+ for (const locale of supportedLocales) {
16
+ expect(Object.keys(messages[locale]).sort()).toEqual(keys);
17
+ }
18
+ });
19
+
20
+ it("loads catalogues from the conventional locale JSON resources", () => {
21
+ expect(messages.sk["catalog.title"]).toBe("Produkty");
22
+ expect(messages.en["catalog.title"]).toBe("Products");
23
+ });
24
+
25
+ it("normalizes regional tags and falls back safely to Slovak", () => {
26
+ expect(parseLocale("en-US")).toBe("en");
27
+ expect(parseLocale("sk-SK")).toBe("sk");
28
+ expect(parseLocale("de")).toBe("sk");
29
+ expect(parseLocale(undefined)).toBe("sk");
30
+ expect(isSupportedLocale("en")).toBe(true);
31
+ expect(isSupportedLocale("en-US")).toBe(false);
32
+ });
33
+
34
+ it("returns localized UI copy without changing commerce locale semantics", () => {
35
+ expect(createTranslator("sk")("checkout.submitPickup")).toBe(
36
+ "Objednať osobný odber na dobierku",
37
+ );
38
+ expect(createTranslator("en")("checkout.submitCourier")).toBe(
39
+ "Place courier COD order",
40
+ );
41
+ expect(createTranslator("en")("checkout.submitPickup")).toBe(
42
+ "Place pickup COD order",
43
+ );
44
+ });
45
+
46
+ it("formats localized navigation variables without exposing a locale branch to UI callers", () => {
47
+ expect(formatMessage("sk", "nav.cartItems", { count: 2 })).toBe("2 ks");
48
+ expect(formatMessage("en", "nav.cartItems", { count: 2 })).toBe("2 items");
49
+ });
50
+ });
@@ -0,0 +1,44 @@
1
+ import englishMessages from "../messages/en.json";
2
+ import slovakMessages from "../messages/sk.json";
3
+
4
+ export const supportedLocales = ["sk", "en"] as const;
5
+
6
+ export type SupportedLocale = (typeof supportedLocales)[number];
7
+ export type MessageKey = keyof typeof slovakMessages;
8
+ export type MessageCatalog = Record<MessageKey, string>;
9
+
10
+ const typedEnglishMessages = englishMessages satisfies MessageCatalog;
11
+
12
+ export const messages = {
13
+ sk: slovakMessages,
14
+ en: typedEnglishMessages,
15
+ } satisfies Record<SupportedLocale, MessageCatalog>;
16
+
17
+ export function isSupportedLocale(
18
+ value: string | undefined,
19
+ ): value is SupportedLocale {
20
+ return value === "sk" || value === "en";
21
+ }
22
+
23
+ export function parseLocale(value: string | undefined): SupportedLocale {
24
+ const language = value?.toLowerCase().split("-")[0];
25
+ return language === "en" ? "en" : "sk";
26
+ }
27
+
28
+ export type Translator = (key: MessageKey) => string;
29
+
30
+ export function createTranslator(value: string | SupportedLocale): Translator {
31
+ const locale = parseLocale(value);
32
+ return (key) => messages[locale][key] ?? messages.sk[key];
33
+ }
34
+
35
+ export function formatMessage(
36
+ value: string | SupportedLocale,
37
+ key: MessageKey,
38
+ variables: Readonly<Record<string, string | number>>,
39
+ ): string {
40
+ return createTranslator(value)(key).replace(
41
+ /\{(\w+)\}/gu,
42
+ (_, name: string) => String(variables[name] ?? `{${name}}`),
43
+ );
44
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ PASSWORD_RESET_TOKEN_COOKIE,
5
+ passwordResetToken,
6
+ passwordResetTokenCookieOptions,
7
+ } from "./password-reset-token";
8
+
9
+ describe("password-reset token bridge", () => {
10
+ it("uses one short-lived HttpOnly cookie that browser JavaScript cannot read", () => {
11
+ expect(PASSWORD_RESET_TOKEN_COOKIE).toBe("jarshop-password-reset-token");
12
+ expect(passwordResetTokenCookieOptions).toMatchObject({
13
+ httpOnly: true,
14
+ maxAge: 15 * 60,
15
+ path: "/",
16
+ sameSite: "lax",
17
+ });
18
+ });
19
+
20
+ it("reads only the reset-token cookie from the server cookie store", () => {
21
+ const store = {
22
+ get(name: string) {
23
+ return name === PASSWORD_RESET_TOKEN_COOKIE
24
+ ? { value: "opaque-reset-token" }
25
+ : undefined;
26
+ },
27
+ };
28
+
29
+ expect(passwordResetToken(store)).toBe("opaque-reset-token");
30
+ });
31
+ });
@@ -0,0 +1,17 @@
1
+ import type { CommerceSessionStore } from "./commerce-session";
2
+
3
+ export const PASSWORD_RESET_TOKEN_COOKIE = "jarshop-password-reset-token";
4
+
5
+ export const passwordResetTokenCookieOptions = {
6
+ httpOnly: true,
7
+ maxAge: 15 * 60,
8
+ path: "/",
9
+ sameSite: "lax" as const,
10
+ secure: process.env.NODE_ENV === "production",
11
+ };
12
+
13
+ export function passwordResetToken(
14
+ store: Pick<CommerceSessionStore, "get">,
15
+ ): string | undefined {
16
+ return store.get(PASSWORD_RESET_TOKEN_COOKIE)?.value;
17
+ }