@jarwizz/create-jarshop 0.1.1 → 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 (107) hide show
  1. package/README.md +10 -2
  2. package/dist/generator.js +10 -4
  3. package/dist/generator.js.map +1 -1
  4. package/dist/template/AGENTS.md +3 -3
  5. package/dist/template/CONTEXT.md +14 -0
  6. package/dist/template/README.md +25 -3
  7. package/dist/template/docker-compose.yml +5 -0
  8. package/dist/template/docs/smtp-smoke.md +37 -0
  9. package/dist/template/env.example.template +18 -0
  10. package/dist/template/gitignore.template +2 -0
  11. package/dist/template/package.json +19 -6
  12. package/dist/template/pnpm-lock.yaml +2389 -98
  13. package/dist/template/pnpm-workspace.yaml +12 -1
  14. package/dist/template/src/dev-storefront.ts +58 -0
  15. package/dist/template/src/dev.ts +67 -4
  16. package/dist/template/src/email-smoke.ts +27 -0
  17. package/dist/template/src/environment.d.ts +6 -0
  18. package/dist/template/src/migrate.ts +16 -0
  19. package/dist/template/src/migrations/1786024108434-jarshop-checkout-attempt.ts +28 -0
  20. package/dist/template/src/migrations/1786024108435-jarshop-legal-and-confirmation.ts +60 -0
  21. package/dist/template/src/migrations/1786024108436-jarshop-order-email-outbox.ts +37 -0
  22. package/dist/template/src/migrations/1786030000000-jarshop-order-email-delivery-lease.ts +27 -0
  23. package/dist/template/src/plugins/jarshop-checkout/checkout-attempt.entity.ts +42 -0
  24. package/dist/template/src/plugins/jarshop-checkout/cod-payment.test.ts +44 -0
  25. package/dist/template/src/plugins/jarshop-checkout/cod-payment.ts +28 -0
  26. package/dist/template/src/plugins/jarshop-checkout/confirmation-token.entity.ts +31 -0
  27. package/dist/template/src/plugins/jarshop-checkout/confirmation.ts +59 -0
  28. package/dist/template/src/plugins/jarshop-checkout/dashboard/index.tsx +107 -0
  29. package/dist/template/src/plugins/jarshop-checkout/dashboard/order-email-delivery-status.ts +43 -0
  30. package/dist/template/src/plugins/jarshop-checkout/email-smoke-core.ts +56 -0
  31. package/dist/template/src/plugins/jarshop-checkout/index.ts +51 -0
  32. package/dist/template/src/plugins/jarshop-checkout/jarshop-checkout.plugin.ts +870 -0
  33. package/dist/template/src/plugins/jarshop-checkout/legal.entity.ts +39 -0
  34. package/dist/template/src/plugins/jarshop-checkout/order-consent.entity.ts +32 -0
  35. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-core.ts +337 -0
  36. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery-status.ts +39 -0
  37. package/dist/template/src/plugins/jarshop-checkout/order-email-delivery.entity.ts +66 -0
  38. package/dist/template/src/plugins/jarshop-checkout/order-email-outbox.ts +108 -0
  39. package/dist/template/src/plugins/jarshop-checkout/order-email-transport.ts +35 -0
  40. package/dist/template/src/plugins/jarshop-checkout/order-email-worker.ts +281 -0
  41. package/dist/template/src/plugins/jarshop-checkout/shipping.test.ts +41 -0
  42. package/dist/template/src/plugins/jarshop-checkout/shipping.ts +162 -0
  43. package/dist/template/src/seed.ts +171 -0
  44. package/dist/template/src/vendure-config.ts +72 -7
  45. package/dist/template/static/email/templates/email-verification/body.hbs +4 -4
  46. package/dist/template/storefront/app/[locale]/[slug]/add-to-cart-form.tsx +44 -0
  47. package/dist/template/storefront/app/[locale]/[slug]/page.tsx +35 -26
  48. package/dist/template/storefront/app/[locale]/account/account-mutation-form.tsx +44 -0
  49. package/dist/template/storefront/app/[locale]/account/change-password-form.tsx +65 -0
  50. package/dist/template/storefront/app/[locale]/account/login/login-form.tsx +60 -0
  51. package/dist/template/storefront/app/[locale]/account/login/page.tsx +23 -0
  52. package/dist/template/storefront/app/[locale]/account/page.tsx +215 -0
  53. package/dist/template/storefront/app/[locale]/account/password-reset/callback/route.ts +33 -0
  54. package/dist/template/storefront/app/[locale]/account/password-reset/complete/page.tsx +27 -0
  55. package/dist/template/storefront/app/[locale]/account/password-reset/page.tsx +37 -0
  56. package/dist/template/storefront/app/[locale]/account/password-reset/password-reset-forms.tsx +99 -0
  57. package/dist/template/storefront/app/[locale]/account/register/page.tsx +23 -0
  58. package/dist/template/storefront/app/[locale]/account/register/register-form.tsx +67 -0
  59. package/dist/template/storefront/app/[locale]/account/verify/complete/route.ts +33 -0
  60. package/dist/template/storefront/app/[locale]/account/verify/page.tsx +41 -0
  61. package/dist/template/storefront/app/[locale]/account/verify/refresh-form.tsx +35 -0
  62. package/dist/template/storefront/app/[locale]/cart/page.tsx +78 -0
  63. package/dist/template/storefront/app/[locale]/checkout/checkout-form.tsx +112 -0
  64. package/dist/template/storefront/app/[locale]/checkout/confirmation/route.ts +21 -0
  65. package/dist/template/storefront/app/[locale]/checkout/page.tsx +44 -0
  66. package/dist/template/storefront/app/[locale]/checkout/success/page.tsx +28 -0
  67. package/dist/template/storefront/app/[locale]/layout.tsx +48 -0
  68. package/dist/template/storefront/app/[locale]/page.tsx +12 -13
  69. package/dist/template/storefront/app/globals.css +372 -4
  70. package/dist/template/storefront/app/layout.tsx +13 -0
  71. package/dist/template/storefront/eslint.config.mjs +8 -1
  72. package/dist/template/storefront/gitignore.template +1 -0
  73. package/dist/template/storefront/lib/account-security.test.ts +74 -0
  74. package/dist/template/storefront/lib/account-security.ts +81 -0
  75. package/dist/template/storefront/lib/account.ts +366 -0
  76. package/dist/template/storefront/lib/cart-contract.ts +52 -0
  77. package/dist/template/storefront/lib/cart.test.ts +30 -0
  78. package/dist/template/storefront/lib/cart.ts +130 -0
  79. package/dist/template/storefront/lib/checkout-contract.test.ts +20 -0
  80. package/dist/template/storefront/lib/checkout-contract.ts +16 -0
  81. package/dist/template/storefront/lib/checkout.ts +149 -0
  82. package/dist/template/storefront/lib/commerce-session.test.ts +77 -0
  83. package/dist/template/storefront/lib/commerce-session.ts +85 -0
  84. package/dist/template/storefront/lib/commerce.ts +129 -4
  85. package/dist/template/storefront/lib/confirmation-cookie.ts +9 -0
  86. package/dist/template/storefront/lib/confirmation.ts +95 -0
  87. package/dist/template/storefront/lib/form-values.test.ts +14 -0
  88. package/dist/template/storefront/lib/form-values.ts +7 -0
  89. package/dist/template/storefront/lib/i18n.test.ts +50 -0
  90. package/dist/template/storefront/lib/i18n.ts +44 -0
  91. package/dist/template/storefront/lib/password-reset-token.test.ts +31 -0
  92. package/dist/template/storefront/lib/password-reset-token.ts +17 -0
  93. package/dist/template/storefront/lib/valkey-rate-limit.ts +72 -0
  94. package/dist/template/storefront/messages/en.json +89 -0
  95. package/dist/template/storefront/messages/sk.json +89 -0
  96. package/dist/template/storefront/next.config.ts +3 -1
  97. package/dist/template/storefront/package.json +11 -4
  98. package/dist/template/storefront/vitest.config.ts +8 -0
  99. package/dist/template/tsconfig.json +2 -1
  100. package/dist/template/turbo.json +8 -0
  101. package/dist/template/vite.config.mts +4 -1
  102. package/dist/template-manifest.json +1 -1
  103. package/dist/template.js +3 -0
  104. package/dist/template.js.map +1 -1
  105. package/package.json +1 -1
  106. package/dist/template/storefront/pnpm-lock.yaml +0 -3562
  107. package/dist/template/storefront/pnpm-workspace.yaml +0 -26
@@ -0,0 +1,149 @@
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 { PLACE_JARSHOP_ORDER_MUTATION } from "./checkout-contract";
12
+ import { exchangeOrderConfirmation } from "./confirmation";
13
+
14
+ export interface CheckoutCartSnapshot {
15
+ currencyCode: string;
16
+ subtotalWithTax: number;
17
+ lines: Array<{
18
+ lineId: string;
19
+ quantity: number;
20
+ linePriceWithTax: number;
21
+ }>;
22
+ }
23
+
24
+ export type CheckoutShippingMethod = "pickup" | "courier";
25
+
26
+ type CheckoutResponse = {
27
+ data?: {
28
+ placeJarShopOrder: {
29
+ success: boolean;
30
+ orderCode: string | null;
31
+ errorCode: string | null;
32
+ message: string | null;
33
+ affectedLineIds: string[];
34
+ confirmationToken: string | null;
35
+ };
36
+ };
37
+ errors?: Array<{ message: string }>;
38
+ };
39
+
40
+ export type CheckoutActionState =
41
+ | { ok: true }
42
+ | {
43
+ ok: false;
44
+ errorCode: string;
45
+ message: string;
46
+ affectedLineIds: string[];
47
+ };
48
+
49
+ export async function placeOrder(
50
+ locale: CommerceLocale,
51
+ attemptId: string,
52
+ cartSnapshot: CheckoutCartSnapshot | null,
53
+ _previousState: CheckoutActionState,
54
+ formData: FormData,
55
+ ): Promise<CheckoutActionState> {
56
+ const store = await cookies();
57
+ const authorization = commerceSessionAuthorization(store);
58
+ const endpoint = new URL(
59
+ process.env.JARSHOP_COMMERCE_ENDPOINT ?? "http://localhost:3000/shop-api",
60
+ );
61
+ endpoint.searchParams.set("languageCode", locale);
62
+ const response = await fetch(endpoint, {
63
+ method: "POST",
64
+ headers: {
65
+ "content-type": "application/json",
66
+ ...(process.env.JARSHOP_CHANNEL_TOKEN
67
+ ? { "vendure-token": process.env.JARSHOP_CHANNEL_TOKEN }
68
+ : {}),
69
+ ...(authorization ? { authorization } : {}),
70
+ },
71
+ body: JSON.stringify({
72
+ query: PLACE_JARSHOP_ORDER_MUTATION,
73
+ variables: {
74
+ input: {
75
+ attemptId,
76
+ marketCode: "sk",
77
+ languageCode: locale,
78
+ currencyCode: "EUR",
79
+ customer: {
80
+ emailAddress: String(formData.get("emailAddress") ?? ""),
81
+ firstName: String(formData.get("firstName") ?? ""),
82
+ lastName: String(formData.get("lastName") ?? ""),
83
+ phoneNumber: String(formData.get("phoneNumber") ?? ""),
84
+ },
85
+ shippingMethodCode: String(
86
+ formData.get("shippingMethodCode") ?? "pickup",
87
+ ),
88
+ paymentMethodCode: "cod",
89
+ termsAccepted: formData.get("termsAccepted") === "on",
90
+ ...(cartSnapshot ? { cartSnapshot } : {}),
91
+ ...(formData.get("shippingMethodCode") === "courier"
92
+ ? {
93
+ shippingAddress: {
94
+ streetLine1: String(formData.get("streetLine1") ?? ""),
95
+ city: String(formData.get("city") ?? ""),
96
+ postalCode: String(formData.get("postalCode") ?? ""),
97
+ countryCode: String(formData.get("countryCode") ?? ""),
98
+ },
99
+ }
100
+ : {}),
101
+ },
102
+ },
103
+ }),
104
+ cache: "no-store",
105
+ });
106
+ persistCommerceSession(store, response.headers);
107
+ if (!response.ok) {
108
+ return {
109
+ ok: false,
110
+ errorCode: "TEMPORARY_FAILURE",
111
+ message: `Checkout API returned ${response.status}`,
112
+ affectedLineIds: [],
113
+ };
114
+ }
115
+ const body = (await response.json()) as CheckoutResponse;
116
+ if (body.errors?.length) {
117
+ return {
118
+ ok: false,
119
+ errorCode: "TEMPORARY_FAILURE",
120
+ message: body.errors[0]?.message ?? "Checkout failed",
121
+ affectedLineIds: [],
122
+ };
123
+ }
124
+ const result = body.data?.placeJarShopOrder;
125
+ if (!result?.success || !result.orderCode || !result.confirmationToken) {
126
+ return {
127
+ ok: false,
128
+ errorCode: result?.errorCode ?? "TEMPORARY_FAILURE",
129
+ message: result?.message ?? "Order could not be placed",
130
+ affectedLineIds: result?.affectedLineIds ?? [],
131
+ };
132
+ }
133
+ const confirmation = await exchangeOrderConfirmation(
134
+ locale,
135
+ result.confirmationToken,
136
+ );
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
+ );
148
+ return { ok: true };
149
+ }
@@ -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,23 @@
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
+ CatalogPage,
10
+ ChangePasswordInput,
3
11
  CommerceResult,
12
+ LoginInput,
13
+ ProductDetail,
14
+ ProductListOptions,
4
15
  ProductSummary,
16
+ RegisterAccountInput,
17
+ ResetPasswordInput,
18
+ SessionStore,
19
+ UpdateAccountProfileInput,
20
+ UpdateAccountAddressInput,
5
21
  } from "@jarwizz/commerce-sdk/types";
6
22
 
7
23
  export type CommerceLocale = "sk" | "en";
@@ -14,22 +30,131 @@ function endpointFor(locale: CommerceLocale): string {
14
30
  return endpoint.toString();
15
31
  }
16
32
 
17
- function client(locale: CommerceLocale) {
33
+ function client(locale: CommerceLocale, sessionStore?: SessionStore) {
18
34
  return createCommerceClient({
19
35
  endpoint: endpointFor(locale),
20
36
  channelToken: process.env.JARSHOP_CHANNEL_TOKEN,
37
+ sessionStore,
21
38
  });
22
39
  }
23
40
 
24
41
  export function getCatalog(
25
42
  locale: CommerceLocale,
26
- ): Promise<CommerceResult<ProductSummary[]>> {
27
- return client(locale).listProducts();
43
+ options?: ProductListOptions,
44
+ ): Promise<CommerceResult<CatalogPage<ProductSummary>>> {
45
+ return client(locale).listProducts(options);
28
46
  }
29
47
 
30
48
  export function getProduct(
31
49
  slug: string,
32
50
  locale: CommerceLocale,
33
- ): Promise<CommerceResult<ProductSummary | null>> {
51
+ ): Promise<CommerceResult<ProductDetail | null>> {
34
52
  return client(locale).getProductBySlug(slug);
35
53
  }
54
+
55
+ export function loginAccount(
56
+ locale: CommerceLocale,
57
+ sessionStore: SessionStore,
58
+ input: LoginInput,
59
+ ): Promise<CommerceResult<AccountSession>> {
60
+ return client(locale, sessionStore).login(input);
61
+ }
62
+
63
+ export function logoutAccount(
64
+ locale: CommerceLocale,
65
+ sessionStore: SessionStore,
66
+ ): Promise<CommerceResult<void>> {
67
+ return client(locale, sessionStore).logout();
68
+ }
69
+
70
+ export function getActiveAccount(
71
+ locale: CommerceLocale,
72
+ sessionStore: SessionStore,
73
+ ): Promise<CommerceResult<AccountSummary | null>> {
74
+ return client(locale, sessionStore).getActiveAccount();
75
+ }
76
+
77
+ export function registerAccount(
78
+ locale: CommerceLocale,
79
+ input: RegisterAccountInput,
80
+ ): Promise<CommerceResult<void>> {
81
+ return client(locale).register(input);
82
+ }
83
+
84
+ export function refreshAccountRegistration(
85
+ locale: CommerceLocale,
86
+ emailAddress: string,
87
+ ): Promise<CommerceResult<void>> {
88
+ return client(locale).refreshRegistration(emailAddress);
89
+ }
90
+
91
+ export function verifyAccountRegistration(
92
+ locale: CommerceLocale,
93
+ sessionStore: SessionStore,
94
+ token: string,
95
+ ): Promise<CommerceResult<AccountSession>> {
96
+ return client(locale, sessionStore).verifyRegistration(token);
97
+ }
98
+
99
+ export function requestAccountPasswordReset(
100
+ locale: CommerceLocale,
101
+ emailAddress: string,
102
+ ): Promise<CommerceResult<void>> {
103
+ return client(locale).requestPasswordReset(emailAddress);
104
+ }
105
+
106
+ export function resetAccountPassword(
107
+ locale: CommerceLocale,
108
+ sessionStore: SessionStore,
109
+ input: ResetPasswordInput,
110
+ ): Promise<CommerceResult<AccountSession>> {
111
+ return client(locale, sessionStore).resetPassword(input);
112
+ }
113
+
114
+ export function getAccountProfile(
115
+ locale: CommerceLocale,
116
+ sessionStore: SessionStore,
117
+ options?: AccountOrdersOptions,
118
+ ): Promise<CommerceResult<AccountProfile | null>> {
119
+ return client(locale, sessionStore).getAccountProfile(options);
120
+ }
121
+
122
+ export function updateAccountProfile(
123
+ locale: CommerceLocale,
124
+ sessionStore: SessionStore,
125
+ input: UpdateAccountProfileInput,
126
+ ): Promise<CommerceResult<AccountSummary>> {
127
+ return client(locale, sessionStore).updateProfile(input);
128
+ }
129
+
130
+ export function createAccountAddress(
131
+ locale: CommerceLocale,
132
+ sessionStore: SessionStore,
133
+ input: AccountAddressInput,
134
+ ): Promise<CommerceResult<AccountAddress>> {
135
+ return client(locale, sessionStore).createAddress(input);
136
+ }
137
+
138
+ export function deleteAccountAddress(
139
+ locale: CommerceLocale,
140
+ sessionStore: SessionStore,
141
+ id: string,
142
+ ): Promise<CommerceResult<void>> {
143
+ return client(locale, sessionStore).deleteAddress(id);
144
+ }
145
+
146
+ export function updateAccountAddress(
147
+ locale: CommerceLocale,
148
+ sessionStore: SessionStore,
149
+ input: UpdateAccountAddressInput,
150
+ ): Promise<CommerceResult<AccountAddress>> {
151
+ return client(locale, sessionStore).updateAddress(input);
152
+ }
153
+
154
+ export function changeAccountPassword(
155
+ locale: CommerceLocale,
156
+ sessionStore: SessionStore,
157
+ input: ChangePasswordInput,
158
+ ): Promise<CommerceResult<void>> {
159
+ return client(locale, sessionStore).changePassword(input);
160
+ }
@@ -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,95 @@
1
+ "use server";
2
+
3
+ import { cookies } from "next/headers";
4
+
5
+ import type { CommerceLocale } from "./commerce";
6
+ import {
7
+ confirmationCookieOptions,
8
+ CONFIRMATION_COOKIE_NAME,
9
+ } from "./confirmation-cookie";
10
+
11
+ export interface OrderConfirmationView {
12
+ orderId: string;
13
+ orderCode: string;
14
+ state: string;
15
+ totalWithTax: number;
16
+ currencyCode: string;
17
+ }
18
+
19
+ interface ConfirmationExchange {
20
+ credential: string;
21
+ confirmation: OrderConfirmationView;
22
+ }
23
+
24
+ function endpointFor(locale: CommerceLocale): string {
25
+ const endpoint = new URL(
26
+ process.env.JARSHOP_COMMERCE_ENDPOINT ?? "http://localhost:3000/shop-api",
27
+ );
28
+ endpoint.searchParams.set("languageCode", locale);
29
+ return endpoint.toString();
30
+ }
31
+
32
+ function commerceHeaders(cookie?: string): HeadersInit {
33
+ return {
34
+ "content-type": "application/json",
35
+ ...(process.env.JARSHOP_CHANNEL_TOKEN
36
+ ? { "vendure-token": process.env.JARSHOP_CHANNEL_TOKEN }
37
+ : {}),
38
+ ...(cookie ? { cookie } : {}),
39
+ };
40
+ }
41
+
42
+ export async function exchangeOrderConfirmation(
43
+ locale: CommerceLocale,
44
+ token: string,
45
+ ): Promise<OrderConfirmationView | null> {
46
+ const store = await cookies();
47
+ const response = await fetch(endpointFor(locale), {
48
+ method: "POST",
49
+ headers: commerceHeaders(),
50
+ body: JSON.stringify({
51
+ query: `mutation ExchangeConfirmation($token: String!) { exchangeJarShopOrderConfirmation(token: $token) { credential confirmation { orderId orderCode state totalWithTax currencyCode } } }`,
52
+ variables: { token },
53
+ }),
54
+ cache: "no-store",
55
+ });
56
+ if (!response.ok) return null;
57
+ const body = (await response.json()) as {
58
+ data?: { exchangeJarShopOrderConfirmation: ConfirmationExchange | null };
59
+ errors?: Array<{ message: string }>;
60
+ };
61
+ const exchange = body.data?.exchangeJarShopOrderConfirmation;
62
+ if (!exchange) return null;
63
+ store.set(
64
+ CONFIRMATION_COOKIE_NAME,
65
+ exchange.credential,
66
+ confirmationCookieOptions,
67
+ );
68
+ return exchange.confirmation;
69
+ }
70
+
71
+ export async function getOrderConfirmation(
72
+ locale: CommerceLocale,
73
+ orderCode: string,
74
+ ): Promise<OrderConfirmationView | null> {
75
+ const store = await cookies();
76
+ const credential = store.get(CONFIRMATION_COOKIE_NAME)?.value;
77
+ if (!credential) return null;
78
+ const response = await fetch(endpointFor(locale), {
79
+ method: "POST",
80
+ headers: commerceHeaders(
81
+ `${CONFIRMATION_COOKIE_NAME}=${encodeURIComponent(credential)}`,
82
+ ),
83
+ body: JSON.stringify({
84
+ query: `query ProtectedConfirmation($orderCode: String!) { jarShopProtectedOrderConfirmation(orderCode: $orderCode) { orderId orderCode state totalWithTax currencyCode } }`,
85
+ variables: { orderCode },
86
+ }),
87
+ cache: "no-store",
88
+ });
89
+ if (!response.ok) return null;
90
+ const body = (await response.json()) as {
91
+ data?: { jarShopProtectedOrderConfirmation: OrderConfirmationView | null };
92
+ errors?: Array<{ message: string }>;
93
+ };
94
+ return body.data?.jarShopProtectedOrderConfirmation ?? null;
95
+ }
@@ -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
+ }