@doswiftly/storefront-sdk 4.3.0 → 4.5.0
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 +6 -14
- package/dist/core/cart/types.d.ts +53 -20
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/cart/types.js +3 -0
- package/dist/core/image.d.ts +4 -46
- package/dist/core/image.d.ts.map +1 -1
- package/dist/core/image.js +4 -65
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +0 -2
- package/dist/core/operations/cart.d.ts +15 -9
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +130 -58
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +9 -4
- package/src/__tests__/contract/storefront-api.contract.test.ts +0 -450
- package/src/__tests__/unit/auth-client.test.ts +0 -210
- package/src/__tests__/unit/bot-protection.test.ts +0 -461
- package/src/__tests__/unit/cart-client.test.ts +0 -233
- package/src/__tests__/unit/cart-store.test.ts +0 -349
- package/src/__tests__/unit/create-client.test.ts +0 -356
- package/src/__tests__/unit/helpers.test.ts +0 -377
- package/src/__tests__/unit/middleware.test.ts +0 -374
- package/src/__tests__/unit/test-helpers.ts +0 -103
- package/src/core/auth/auth-client.ts +0 -123
- package/src/core/auth/cookie-config.ts +0 -23
- package/src/core/auth/handlers.ts +0 -168
- package/src/core/auth/routes.ts +0 -26
- package/src/core/auth/token-client.ts +0 -51
- package/src/core/auth/types.ts +0 -54
- package/src/core/bot-protection/abstract-manager.ts +0 -185
- package/src/core/bot-protection/create-manager.ts +0 -37
- package/src/core/bot-protection/eucaptcha-manager.ts +0 -88
- package/src/core/bot-protection/fallback-manager.ts +0 -43
- package/src/core/bot-protection/turnstile-manager.ts +0 -92
- package/src/core/bot-protection/types/eucaptcha.d.ts +0 -28
- package/src/core/bot-protection/types/turnstile.d.ts +0 -33
- package/src/core/cache.ts +0 -102
- package/src/core/cart/cart-client.ts +0 -150
- package/src/core/cart/cookie-config.ts +0 -13
- package/src/core/cart/types.ts +0 -104
- package/src/core/client/compose.ts +0 -15
- package/src/core/client/create-client.ts +0 -129
- package/src/core/client/dedupe.ts +0 -19
- package/src/core/client/execute.ts +0 -70
- package/src/core/client/hash.ts +0 -21
- package/src/core/client/operation-name.ts +0 -12
- package/src/core/client/types.ts +0 -171
- package/src/core/currency/cookie-config.ts +0 -13
- package/src/core/errors.ts +0 -67
- package/src/core/format.ts +0 -254
- package/src/core/helpers/assert-no-user-errors.ts +0 -21
- package/src/core/helpers/normalize-connection.ts +0 -48
- package/src/core/helpers/sanitize-html.ts +0 -42
- package/src/core/image.ts +0 -103
- package/src/core/index.ts +0 -180
- package/src/core/language/cookie-config.ts +0 -13
- package/src/core/middleware/auth.ts +0 -27
- package/src/core/middleware/bot-protection.ts +0 -140
- package/src/core/middleware/currency.ts +0 -27
- package/src/core/middleware/errors.ts +0 -86
- package/src/core/middleware/language.ts +0 -30
- package/src/core/middleware/retry.ts +0 -75
- package/src/core/middleware/timeout.ts +0 -61
- package/src/core/operations/auth.ts +0 -123
- package/src/core/operations/cart.ts +0 -185
- package/src/index.ts +0 -25
- package/src/react/bot-protection/bot-protection-context.ts +0 -17
- package/src/react/bot-protection/bot-protection-widget.tsx +0 -46
- package/src/react/cookies.ts +0 -89
- package/src/react/helpers/create-store-context.ts +0 -56
- package/src/react/hooks/use-auth.ts +0 -218
- package/src/react/hooks/use-bot-protection.ts +0 -31
- package/src/react/hooks/use-cart-manager.ts +0 -236
- package/src/react/hooks/use-currency.ts +0 -23
- package/src/react/hooks/use-debounced-value.ts +0 -30
- package/src/react/hooks/use-hydrated.ts +0 -20
- package/src/react/hooks/use-storefront-client.ts +0 -12
- package/src/react/index.ts +0 -71
- package/src/react/providers/currency-provider.tsx +0 -30
- package/src/react/providers/language-provider.tsx +0 -34
- package/src/react/providers/storefront-client-provider.tsx +0 -107
- package/src/react/providers/storefront-provider.tsx +0 -99
- package/src/react/server/get-storefront-client.ts +0 -60
- package/src/react/server/index.ts +0 -1
- package/src/react/stores/auth.store.ts +0 -112
- package/src/react/stores/cart.context.ts +0 -10
- package/src/react/stores/cart.store.ts +0 -254
- package/src/react/stores/currency.store.ts +0 -93
- package/src/react/stores/index.ts +0 -17
- package/src/react/stores/language.store.ts +0 -90
- package/src/react/stores/store-context.tsx +0 -103
- package/src/react/types/shop-config.ts +0 -22
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -14
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Native fetch transport — 0 runtime dependencies.
|
|
3
|
-
*
|
|
4
|
-
* Sends GraphQL POST request, parses response, returns typed GraphQLResponse.
|
|
5
|
-
* Error normalization is handled by error middleware, NOT here.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { GraphQLRequest, GraphQLResponse, GraphQLErrorInfo } from './types';
|
|
9
|
-
|
|
10
|
-
export interface ExecuteConfig {
|
|
11
|
-
/** GraphQL endpoint URL */
|
|
12
|
-
endpoint: string;
|
|
13
|
-
/** Custom fetch implementation */
|
|
14
|
-
fetch: typeof globalThis.fetch;
|
|
15
|
-
/** Enable debug logging */
|
|
16
|
-
debug: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create the innermost execute function for the middleware pipeline.
|
|
21
|
-
*/
|
|
22
|
-
export function createExecute(config: ExecuteConfig) {
|
|
23
|
-
const { endpoint, fetch: fetchFn, debug } = config;
|
|
24
|
-
|
|
25
|
-
return async function execute(request: GraphQLRequest): Promise<GraphQLResponse> {
|
|
26
|
-
const { query, variables, headers, signal, operationName } = request;
|
|
27
|
-
|
|
28
|
-
if (debug) {
|
|
29
|
-
console.log('[StorefrontSDK]', operationName ?? 'request', { variables });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const body: Record<string, unknown> = { query };
|
|
33
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
34
|
-
body.variables = variables;
|
|
35
|
-
}
|
|
36
|
-
if (operationName) {
|
|
37
|
-
body.operationName = operationName;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const response = await fetchFn(endpoint, {
|
|
41
|
-
method: 'POST',
|
|
42
|
-
headers: {
|
|
43
|
-
'Content-Type': 'application/json',
|
|
44
|
-
Accept: 'application/json',
|
|
45
|
-
...headers,
|
|
46
|
-
},
|
|
47
|
-
body: JSON.stringify(body),
|
|
48
|
-
signal,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const json = await response.json() as {
|
|
52
|
-
data?: unknown;
|
|
53
|
-
errors?: GraphQLErrorInfo[];
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
if (debug) {
|
|
57
|
-
console.log('[StorefrontSDK]', operationName ?? 'response', {
|
|
58
|
-
status: response.status,
|
|
59
|
-
hasErrors: !!json.errors?.length,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
data: json.data as never,
|
|
65
|
-
errors: json.errors,
|
|
66
|
-
status: response.status,
|
|
67
|
-
headers: response.headers,
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
}
|
package/src/core/client/hash.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stable query hashing for request deduplication.
|
|
3
|
-
*
|
|
4
|
-
* Sorts object keys so `{a:1,b:2}` and `{b:2,a:1}` produce the same hash.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
function stableStringify(value: unknown): string {
|
|
8
|
-
if (value === null || value === undefined) return '';
|
|
9
|
-
if (typeof value !== 'object') return JSON.stringify(value);
|
|
10
|
-
if (Array.isArray(value)) {
|
|
11
|
-
return '[' + value.map(stableStringify).join(',') + ']';
|
|
12
|
-
}
|
|
13
|
-
const keys = Object.keys(value as Record<string, unknown>).sort();
|
|
14
|
-
return '{' + keys.map(k => JSON.stringify(k) + ':' + stableStringify((value as Record<string, unknown>)[k])).join(',') + '}';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function hashQuery(query: string, variables?: Record<string, unknown>): string {
|
|
18
|
-
const queryPart = query.trim();
|
|
19
|
-
const varsPart = variables ? stableStringify(variables) : '';
|
|
20
|
-
return queryPart + '::' + varsPart;
|
|
21
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-extract operation name from a GraphQL query string.
|
|
3
|
-
*
|
|
4
|
-
* Used for: X-Operation-Name header (debugging), cache tags, logging.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const OPERATION_RE = /(?:query|mutation|subscription)\s+(\w+)/;
|
|
8
|
-
|
|
9
|
-
export function getOperationName(query: string): string {
|
|
10
|
-
const match = query.match(OPERATION_RE);
|
|
11
|
-
return match?.[1] ?? 'anonymous';
|
|
12
|
-
}
|
package/src/core/client/types.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core types for Storefront SDK transport layer.
|
|
3
|
-
*
|
|
4
|
-
* Framework-agnostic — no React, no Zustand, 0 runtime dependencies.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// TypedDocumentString (Saleor / client-preset pattern)
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A branded string carrying result & variable types.
|
|
13
|
-
* Produced by graphql-codegen client-preset with `documentMode: 'string'`.
|
|
14
|
-
*
|
|
15
|
-
* SDK also accepts plain strings — TypedDocumentString is purely for DX.
|
|
16
|
-
*/
|
|
17
|
-
export class TypedDocumentString<TResult = unknown, TVariables = unknown> extends String {
|
|
18
|
-
/** Type-level brand — never used at runtime */
|
|
19
|
-
__apiType?: (variables: TVariables) => TResult;
|
|
20
|
-
|
|
21
|
-
constructor(value: string, public __meta__?: { hash: string }) {
|
|
22
|
-
super(value);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
override toString(): string {
|
|
26
|
-
return this.valueOf();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// GraphQL request / response
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
export interface GraphQLRequest {
|
|
35
|
-
/** GraphQL query or mutation string */
|
|
36
|
-
query: string;
|
|
37
|
-
/** Operation variables */
|
|
38
|
-
variables?: Record<string, unknown>;
|
|
39
|
-
/** Operation name (auto-extracted if omitted) */
|
|
40
|
-
operationName?: string;
|
|
41
|
-
/** Request headers (merged: default < middleware < request) */
|
|
42
|
-
headers: Record<string, string>;
|
|
43
|
-
/** AbortSignal for cancellation */
|
|
44
|
-
signal?: AbortSignal;
|
|
45
|
-
/** Whether this is a mutation (affects retry, cache) */
|
|
46
|
-
isMutation: boolean;
|
|
47
|
-
/** Cache strategy override */
|
|
48
|
-
cache?: CacheStrategy;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface GraphQLResponse<T = unknown> {
|
|
52
|
-
/** Parsed response data */
|
|
53
|
-
data: T;
|
|
54
|
-
/** GraphQL errors (if any) */
|
|
55
|
-
errors?: GraphQLErrorInfo[];
|
|
56
|
-
/** HTTP status code */
|
|
57
|
-
status: number;
|
|
58
|
-
/** Raw response headers */
|
|
59
|
-
headers: Headers;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface GraphQLErrorInfo {
|
|
63
|
-
message: string;
|
|
64
|
-
locations?: Array<{ line: number; column: number }>;
|
|
65
|
-
path?: Array<string | number>;
|
|
66
|
-
extensions?: Record<string, unknown>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// User errors (cart mutations, auth mutations, etc.)
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
export interface UserError {
|
|
74
|
-
message: string;
|
|
75
|
-
field?: string[];
|
|
76
|
-
code?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Middleware
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Execute function — the innermost (or next) handler in the pipeline.
|
|
85
|
-
*/
|
|
86
|
-
export type ExecuteFn = (request: GraphQLRequest) => Promise<GraphQLResponse>;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Middleware function — receives the request and the next handler.
|
|
90
|
-
*
|
|
91
|
-
* Can modify request, modify response, short-circuit, retry, etc.
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```typescript
|
|
95
|
-
* const authMiddleware: Middleware = (req, next) => {
|
|
96
|
-
* req.headers['Authorization'] = `Bearer ${getToken()}`;
|
|
97
|
-
* return next(req);
|
|
98
|
-
* };
|
|
99
|
-
* ```
|
|
100
|
-
*/
|
|
101
|
-
export type Middleware = (request: GraphQLRequest, next: ExecuteFn) => Promise<GraphQLResponse>;
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// Cache strategy
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
export interface CacheOptions {
|
|
108
|
-
/** Max age in seconds */
|
|
109
|
-
maxAge: number;
|
|
110
|
-
/** Stale-while-revalidate in seconds */
|
|
111
|
-
staleWhileRevalidate?: number;
|
|
112
|
-
/** Cache mode */
|
|
113
|
-
mode: 'public' | 'private' | 'no-store';
|
|
114
|
-
/** Cache tags (for Next.js revalidateTag) */
|
|
115
|
-
tags?: string[];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export type CacheStrategy = CacheOptions;
|
|
119
|
-
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Client config
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
export interface StorefrontClientConfig {
|
|
125
|
-
/** GraphQL API URL (e.g. 'https://api.doswiftly.pl') */
|
|
126
|
-
apiUrl: string;
|
|
127
|
-
/** Shop slug for multi-tenant routing */
|
|
128
|
-
shopSlug: string;
|
|
129
|
-
/** Default headers for all requests */
|
|
130
|
-
defaultHeaders?: Record<string, string>;
|
|
131
|
-
/** Middleware pipeline */
|
|
132
|
-
middleware?: Middleware[];
|
|
133
|
-
/** Custom fetch implementation (polyfill, test mocks, edge) */
|
|
134
|
-
fetch?: typeof globalThis.fetch;
|
|
135
|
-
/** Enable debug logging (request/response in dev) */
|
|
136
|
-
debug?: boolean;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
// Client interface
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
|
|
143
|
-
export interface StorefrontClient {
|
|
144
|
-
/**
|
|
145
|
-
* Execute a typed GraphQL query.
|
|
146
|
-
*
|
|
147
|
-
* Accepts TypedDocumentString (from codegen) or plain string.
|
|
148
|
-
*/
|
|
149
|
-
query<T = unknown, V = Record<string, unknown>>(
|
|
150
|
-
document: TypedDocumentString<T, V> | string,
|
|
151
|
-
variables?: V,
|
|
152
|
-
cache?: CacheStrategy,
|
|
153
|
-
): Promise<T>;
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Execute a typed GraphQL mutation.
|
|
157
|
-
*
|
|
158
|
-
* Mutations are never cached and never retried by retry middleware.
|
|
159
|
-
*/
|
|
160
|
-
mutate<T = unknown, V = Record<string, unknown>>(
|
|
161
|
-
document: TypedDocumentString<T, V> | string,
|
|
162
|
-
variables?: V,
|
|
163
|
-
): Promise<T>;
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Add middleware to the pipeline (imperative API).
|
|
167
|
-
*
|
|
168
|
-
* Invalidates the compiled pipeline — next request re-composes.
|
|
169
|
-
*/
|
|
170
|
-
use(middleware: Middleware): void;
|
|
171
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Currency configuration constants — platform contract.
|
|
3
|
-
*
|
|
4
|
-
* Used by:
|
|
5
|
-
* - SDK currency store (client-side cookie read/write)
|
|
6
|
-
* - SDK currency middleware (X-Preferred-Currency header)
|
|
7
|
-
* - Server-side currency detection (SSR helpers)
|
|
8
|
-
*
|
|
9
|
-
* Single cookie for preferred currency persistence.
|
|
10
|
-
*/
|
|
11
|
-
export const CURRENCY_COOKIE_NAME = 'preferred-currency';
|
|
12
|
-
export const CURRENCY_COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year
|
|
13
|
-
export const CURRENCY_HEADER_NAME = 'X-Preferred-Currency';
|
package/src/core/errors.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StorefrontError — unified error class for all SDK errors.
|
|
3
|
-
*
|
|
4
|
-
* Normalizes GraphQL errors, network errors, user errors, and timeouts
|
|
5
|
-
* into a single throwable type with structured data.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { GraphQLErrorInfo, UserError } from './client/types';
|
|
9
|
-
|
|
10
|
-
export interface StorefrontErrorOptions {
|
|
11
|
-
/** Error code for programmatic handling */
|
|
12
|
-
code: string;
|
|
13
|
-
/** Human-readable message */
|
|
14
|
-
message: string;
|
|
15
|
-
/** HTTP status code (0 for network errors) */
|
|
16
|
-
status?: number;
|
|
17
|
-
/** GraphQL-level errors from the response */
|
|
18
|
-
graphqlErrors?: GraphQLErrorInfo[];
|
|
19
|
-
/** User errors from mutations (field-level validation) */
|
|
20
|
-
userErrors?: UserError[];
|
|
21
|
-
/** Original error (if wrapping) */
|
|
22
|
-
cause?: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class StorefrontError extends Error {
|
|
26
|
-
readonly code: string;
|
|
27
|
-
readonly status: number;
|
|
28
|
-
readonly graphqlErrors: GraphQLErrorInfo[];
|
|
29
|
-
readonly userErrors: UserError[];
|
|
30
|
-
|
|
31
|
-
constructor(options: StorefrontErrorOptions) {
|
|
32
|
-
super(options.message, { cause: options.cause });
|
|
33
|
-
this.name = 'StorefrontError';
|
|
34
|
-
this.code = options.code;
|
|
35
|
-
this.status = options.status ?? 0;
|
|
36
|
-
this.graphqlErrors = options.graphqlErrors ?? [];
|
|
37
|
-
this.userErrors = options.userErrors ?? [];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** True if this error contains user errors (field-level validation) */
|
|
41
|
-
get hasUserErrors(): boolean {
|
|
42
|
-
return this.userErrors.length > 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** True if this is a network error (no HTTP response) */
|
|
46
|
-
get isNetworkError(): boolean {
|
|
47
|
-
return this.code === 'NETWORK_ERROR';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** True if the request timed out */
|
|
51
|
-
get isTimeout(): boolean {
|
|
52
|
-
return this.code === 'TIMEOUT';
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Error codes
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
export const ErrorCodes = {
|
|
61
|
-
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
62
|
-
TIMEOUT: 'TIMEOUT',
|
|
63
|
-
HTTP_ERROR: 'HTTP_ERROR',
|
|
64
|
-
GRAPHQL_ERROR: 'GRAPHQL_ERROR',
|
|
65
|
-
USER_ERROR: 'USER_ERROR',
|
|
66
|
-
NO_DATA: 'NO_DATA',
|
|
67
|
-
} as const;
|
package/src/core/format.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Formatting Utilities
|
|
3
|
-
*
|
|
4
|
-
* Pure functions for formatting prices, dates, numbers.
|
|
5
|
-
* 0 runtime dependencies — works in Node.js, Edge, Deno, Bun.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// ============================================================================
|
|
9
|
-
// TYPES
|
|
10
|
-
// ============================================================================
|
|
11
|
-
|
|
12
|
-
export interface PriceMoney {
|
|
13
|
-
amount: string;
|
|
14
|
-
currencyCode: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ============================================================================
|
|
18
|
-
// CONSTANTS
|
|
19
|
-
// ============================================================================
|
|
20
|
-
|
|
21
|
-
/** Currency symbols mapping */
|
|
22
|
-
export const CURRENCY_SYMBOLS: Record<string, string> = {
|
|
23
|
-
PLN: 'zł',
|
|
24
|
-
EUR: '€',
|
|
25
|
-
USD: '$',
|
|
26
|
-
GBP: '£',
|
|
27
|
-
CHF: 'CHF',
|
|
28
|
-
CZK: 'Kč',
|
|
29
|
-
SEK: 'kr',
|
|
30
|
-
NOK: 'kr',
|
|
31
|
-
DKK: 'kr',
|
|
32
|
-
JPY: '¥',
|
|
33
|
-
CNY: '¥',
|
|
34
|
-
AUD: 'A$',
|
|
35
|
-
CAD: 'C$',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** Currency locale mapping for proper formatting */
|
|
39
|
-
export const CURRENCY_LOCALES: Record<string, string> = {
|
|
40
|
-
PLN: 'pl-PL',
|
|
41
|
-
EUR: 'de-DE',
|
|
42
|
-
USD: 'en-US',
|
|
43
|
-
GBP: 'en-GB',
|
|
44
|
-
CHF: 'de-CH',
|
|
45
|
-
CZK: 'cs-CZ',
|
|
46
|
-
SEK: 'sv-SE',
|
|
47
|
-
NOK: 'nb-NO',
|
|
48
|
-
DKK: 'da-DK',
|
|
49
|
-
JPY: 'ja-JP',
|
|
50
|
-
CNY: 'zh-CN',
|
|
51
|
-
AUD: 'en-AU',
|
|
52
|
-
CAD: 'en-CA',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// ============================================================================
|
|
56
|
-
// FORMATTER CACHE (avoids re-creating Intl objects on every call)
|
|
57
|
-
// ============================================================================
|
|
58
|
-
|
|
59
|
-
const numberFormatCache = new Map<string, Intl.NumberFormat>();
|
|
60
|
-
const dateFormatCache = new Map<string, Intl.DateTimeFormat>();
|
|
61
|
-
|
|
62
|
-
function getCurrencyFormatter(locale: string, currency: string): Intl.NumberFormat {
|
|
63
|
-
const key = `${locale}:${currency}`;
|
|
64
|
-
let fmt = numberFormatCache.get(key);
|
|
65
|
-
if (!fmt) {
|
|
66
|
-
fmt = new Intl.NumberFormat(locale, {
|
|
67
|
-
style: 'currency',
|
|
68
|
-
currency,
|
|
69
|
-
minimumFractionDigits: 2,
|
|
70
|
-
maximumFractionDigits: 2,
|
|
71
|
-
});
|
|
72
|
-
numberFormatCache.set(key, fmt);
|
|
73
|
-
}
|
|
74
|
-
return fmt;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getNumberFormatter(locale: string): Intl.NumberFormat {
|
|
78
|
-
let fmt = numberFormatCache.get(locale);
|
|
79
|
-
if (!fmt) {
|
|
80
|
-
fmt = new Intl.NumberFormat(locale);
|
|
81
|
-
numberFormatCache.set(locale, fmt);
|
|
82
|
-
}
|
|
83
|
-
return fmt;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function getDateFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
|
|
87
|
-
const key = `${locale}:${JSON.stringify(options)}`;
|
|
88
|
-
let fmt = dateFormatCache.get(key);
|
|
89
|
-
if (!fmt) {
|
|
90
|
-
fmt = new Intl.DateTimeFormat(locale, options);
|
|
91
|
-
dateFormatCache.set(key, fmt);
|
|
92
|
-
}
|
|
93
|
-
return fmt;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ============================================================================
|
|
97
|
-
// UTILITY FUNCTIONS
|
|
98
|
-
// ============================================================================
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Get currency symbol
|
|
102
|
-
*/
|
|
103
|
-
export function getCurrencySymbol(code: string): string {
|
|
104
|
-
return CURRENCY_SYMBOLS[code] || code;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ============================================================================
|
|
108
|
-
// PRICE FORMATTING
|
|
109
|
-
// ============================================================================
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Format price with currency symbol
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* ```typescript
|
|
116
|
-
* formatPrice({ amount: "99.99", currencyCode: "USD" })
|
|
117
|
-
* // => "$99.99"
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
export function formatPrice(price: PriceMoney | null | undefined): string {
|
|
121
|
-
if (!price) return '';
|
|
122
|
-
|
|
123
|
-
const amount = parseFloat(price.amount);
|
|
124
|
-
const code = price.currencyCode;
|
|
125
|
-
const locale = CURRENCY_LOCALES[code] || 'en-US';
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
return getCurrencyFormatter(locale, code).format(amount);
|
|
129
|
-
} catch {
|
|
130
|
-
const symbol = CURRENCY_SYMBOLS[code] || code;
|
|
131
|
-
return `${amount.toFixed(2)} ${symbol}`;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Format price range
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* ```typescript
|
|
140
|
-
* formatPriceRange(
|
|
141
|
-
* { amount: "10.00", currencyCode: "USD" },
|
|
142
|
-
* { amount: "50.00", currencyCode: "USD" }
|
|
143
|
-
* )
|
|
144
|
-
* // => "$10.00 - $50.00"
|
|
145
|
-
* ```
|
|
146
|
-
*/
|
|
147
|
-
export function formatPriceRange(
|
|
148
|
-
minPrice: PriceMoney,
|
|
149
|
-
maxPrice: PriceMoney,
|
|
150
|
-
): string {
|
|
151
|
-
if (minPrice.amount === maxPrice.amount) {
|
|
152
|
-
return formatPrice(minPrice);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return `${formatPrice(minPrice)} - ${formatPrice(maxPrice)}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Format amount with currency
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* ```tsx
|
|
163
|
-
* const formatted = formatAmount("115.20", "EUR");
|
|
164
|
-
* // => "115,20 €"
|
|
165
|
-
* ```
|
|
166
|
-
*/
|
|
167
|
-
export function formatAmount(
|
|
168
|
-
amount: string | number,
|
|
169
|
-
currencyCode: string,
|
|
170
|
-
): string {
|
|
171
|
-
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
|
172
|
-
const locale = CURRENCY_LOCALES[currencyCode] || 'en-US';
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
return getCurrencyFormatter(locale, currencyCode).format(numAmount);
|
|
176
|
-
} catch {
|
|
177
|
-
const symbol = CURRENCY_SYMBOLS[currencyCode] || currencyCode;
|
|
178
|
-
return `${numAmount.toFixed(2)} ${symbol}`;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// DATE FORMATTING
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Format date to locale string
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* ```typescript
|
|
191
|
-
* formatDate(new Date())
|
|
192
|
-
* // => "Dec 9, 2025"
|
|
193
|
-
* ```
|
|
194
|
-
*/
|
|
195
|
-
export function formatDate(date: Date | string): string {
|
|
196
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
197
|
-
|
|
198
|
-
return getDateFormatter('en-US', {
|
|
199
|
-
year: 'numeric',
|
|
200
|
-
month: 'short',
|
|
201
|
-
day: 'numeric',
|
|
202
|
-
}).format(d);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Format date with time
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* ```typescript
|
|
210
|
-
* formatDateTime(new Date())
|
|
211
|
-
* // => "Dec 9, 2025, 10:30 PM"
|
|
212
|
-
* ```
|
|
213
|
-
*/
|
|
214
|
-
export function formatDateTime(date: Date | string): string {
|
|
215
|
-
const d = typeof date === 'string' ? new Date(date) : date;
|
|
216
|
-
|
|
217
|
-
return getDateFormatter('en-US', {
|
|
218
|
-
year: 'numeric',
|
|
219
|
-
month: 'short',
|
|
220
|
-
day: 'numeric',
|
|
221
|
-
hour: 'numeric',
|
|
222
|
-
minute: '2-digit',
|
|
223
|
-
}).format(d);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ============================================================================
|
|
227
|
-
// NUMBER FORMATTING
|
|
228
|
-
// ============================================================================
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Format number with thousands separator
|
|
232
|
-
*
|
|
233
|
-
* @example
|
|
234
|
-
* ```typescript
|
|
235
|
-
* formatNumber(1234567)
|
|
236
|
-
* // => "1,234,567"
|
|
237
|
-
* ```
|
|
238
|
-
*/
|
|
239
|
-
export function formatNumber(num: number): string {
|
|
240
|
-
return getNumberFormatter('en-US').format(num);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Format percentage
|
|
245
|
-
*
|
|
246
|
-
* @example
|
|
247
|
-
* ```typescript
|
|
248
|
-
* formatPercentage(0.15)
|
|
249
|
-
* // => "15%"
|
|
250
|
-
* ```
|
|
251
|
-
*/
|
|
252
|
-
export function formatPercentage(value: number): string {
|
|
253
|
-
return `${Math.round(value * 100)}%`;
|
|
254
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-throw on userErrors — used internally by CartClient and AuthClient.
|
|
3
|
-
*
|
|
4
|
-
* Frontend code doesn't need to manually check `if (userErrors.length)`.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { UserError } from '../client/types';
|
|
8
|
-
import { StorefrontError, ErrorCodes } from '../errors';
|
|
9
|
-
|
|
10
|
-
export function assertNoUserErrors(
|
|
11
|
-
result: { userErrors?: UserError[] | null },
|
|
12
|
-
): void {
|
|
13
|
-
if (result.userErrors && result.userErrors.length > 0) {
|
|
14
|
-
const firstError = result.userErrors[0];
|
|
15
|
-
throw new StorefrontError({
|
|
16
|
-
code: ErrorCodes.USER_ERROR,
|
|
17
|
-
message: firstError.message,
|
|
18
|
-
userErrors: result.userErrors,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Relay-style GraphQL connection normalizer.
|
|
3
|
-
*
|
|
4
|
-
* Converts `{ edges: [{ node }], pageInfo, totalCount }` → flat array.
|
|
5
|
-
* Pure function, 0 deps, works everywhere.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface ConnectionEdge<T> {
|
|
9
|
-
node: T;
|
|
10
|
-
cursor: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ConnectionPageInfo {
|
|
14
|
-
hasNextPage: boolean;
|
|
15
|
-
hasPreviousPage: boolean;
|
|
16
|
-
startCursor?: string | null;
|
|
17
|
-
endCursor?: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface Connection<T> {
|
|
21
|
-
edges: ConnectionEdge<T>[];
|
|
22
|
-
pageInfo: ConnectionPageInfo;
|
|
23
|
-
totalCount?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface NormalizedConnection<T> {
|
|
27
|
-
items: T[];
|
|
28
|
-
pageInfo: ConnectionPageInfo;
|
|
29
|
-
totalCount?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Normalize a GraphQL Relay connection to a flat array.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```typescript
|
|
37
|
-
* const { items, pageInfo, totalCount } = normalizeConnection(data.products);
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export function normalizeConnection<T>(
|
|
41
|
-
connection: Connection<T>,
|
|
42
|
-
): NormalizedConnection<T> {
|
|
43
|
-
return {
|
|
44
|
-
items: connection.edges.map((edge) => edge.node),
|
|
45
|
-
pageInfo: connection.pageInfo,
|
|
46
|
-
totalCount: connection.totalCount,
|
|
47
|
-
};
|
|
48
|
-
}
|