@doswiftly/cli 0.2.6 → 1.0.1

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 (42) hide show
  1. package/CHANGELOG.md +650 -0
  2. package/dist/commands/deploy.d.ts.map +1 -1
  3. package/dist/commands/deploy.js +6 -1
  4. package/dist/commands/deploy.js.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/storefront-nextjs-shadcn/.github/workflows/build-template.yml +2 -1
  7. package/templates/storefront-nextjs-shadcn/.github/workflows/deploy.yml +5 -1
  8. package/templates/storefront-nextjs-shadcn/.github/workflows/preview.yml +3 -1
  9. package/templates/storefront-nextjs-shadcn/app/[locale]/account/addresses/page.tsx +10 -10
  10. package/templates/storefront-nextjs-shadcn/app/[locale]/account/loyalty/page.tsx +1 -1
  11. package/templates/storefront-nextjs-shadcn/app/[locale]/account/orders/[id]/page.tsx +9 -9
  12. package/templates/storefront-nextjs-shadcn/app/[locale]/account/orders/[id]/tracking/page.tsx +1 -1
  13. package/templates/storefront-nextjs-shadcn/app/[locale]/account/orders/page.tsx +2 -2
  14. package/templates/storefront-nextjs-shadcn/app/[locale]/account/settings/page.tsx +3 -3
  15. package/templates/storefront-nextjs-shadcn/app/[locale]/categories/page.tsx +1 -1
  16. package/templates/storefront-nextjs-shadcn/app/[locale]/checkout/page.tsx +74 -74
  17. package/templates/storefront-nextjs-shadcn/app/[locale]/products/[slug]/product-client.tsx +1 -1
  18. package/templates/storefront-nextjs-shadcn/app/[locale]/products/products-client.tsx +4 -4
  19. package/templates/storefront-nextjs-shadcn/app/api/auth/whoami/route.ts +6 -0
  20. package/templates/storefront-nextjs-shadcn/components/account/address-form.tsx +43 -43
  21. package/templates/storefront-nextjs-shadcn/components/account/address-list.tsx +9 -9
  22. package/templates/storefront-nextjs-shadcn/components/account/customer-info.fragment.graphql +5 -5
  23. package/templates/storefront-nextjs-shadcn/components/account/order-details.tsx +8 -8
  24. package/templates/storefront-nextjs-shadcn/components/account/order-history.tsx +2 -2
  25. package/templates/storefront-nextjs-shadcn/components/account/order-summary.fragment.graphql +15 -13
  26. package/templates/storefront-nextjs-shadcn/components/cart/cart-line.fragment.graphql +6 -6
  27. package/templates/storefront-nextjs-shadcn/components/discount/discount-code-input.tsx +10 -10
  28. package/templates/storefront-nextjs-shadcn/components/gift-card/gift-card-input.tsx +9 -9
  29. package/templates/storefront-nextjs-shadcn/components/order/delivery-estimate.tsx +2 -2
  30. package/templates/storefront-nextjs-shadcn/components/product/product-card.tsx +1 -1
  31. package/templates/storefront-nextjs-shadcn/components/seo/product-json-ld.ts +1 -1
  32. package/templates/storefront-nextjs-shadcn/hooks/use-auth-sync.ts +43 -25
  33. package/templates/storefront-nextjs-shadcn/hooks/use-cart-actions.ts +1 -1
  34. package/templates/storefront-nextjs-shadcn/hooks/use-cart-sync.ts +14 -14
  35. package/templates/storefront-nextjs-shadcn/lib/graphql/hooks.ts +101 -112
  36. package/templates/storefront-nextjs-shadcn/lib/graphql/query-keys.ts +2 -2
  37. package/templates/storefront-nextjs-shadcn/lib/graphql/server.ts +29 -12
  38. package/templates/storefront-nextjs-shadcn/lib/graphql/types.ts +1 -1
  39. package/templates/storefront-nextjs-shadcn/messages/en.json +6 -6
  40. package/templates/storefront-nextjs-shadcn/messages/pl.json +6 -6
  41. package/templates/storefront-nextjs-shadcn/package.json +4 -1
  42. package/templates/storefront-nextjs-shadcn/stores/checkout-store.ts +8 -8
@@ -97,7 +97,7 @@ export function OrderHistory({ orders, className }: OrderHistoryProps) {
97
97
  <div className="space-y-1 text-sm text-muted-foreground">
98
98
  <p>{t("placedOn")} {formatDate(order.processedAt)}</p>
99
99
  <p>
100
- {t("itemCount", { count: order.lineItemsCount })}
100
+ {t("itemCount", { count: order.itemCount })}
101
101
  </p>
102
102
  </div>
103
103
  </div>
@@ -105,7 +105,7 @@ export function OrderHistory({ orders, className }: OrderHistoryProps) {
105
105
  <div className="text-right">
106
106
  <p className="text-sm text-muted-foreground">{t("orderTotal")}</p>
107
107
  <p className="text-lg font-semibold text-foreground">
108
- {formatAmount(order.totalPrice.amount, order.totalPrice.currencyCode)}
108
+ {formatAmount(order.totals.total.amount, order.totals.total.currencyCode)}
109
109
  </p>
110
110
  </div>
111
111
  <ChevronRight className="h-5 w-5 text-muted-foreground" />
@@ -11,21 +11,23 @@ fragment OrderSummaryFields on Order {
11
11
  id
12
12
  orderNumber
13
13
  status
14
- financialStatus
14
+ paymentStatus
15
15
  fulfillmentStatus
16
16
  processedAt
17
- lineItemsCount
18
- totalPrice {
19
- amount
20
- currencyCode
21
- }
22
- subtotalPrice {
23
- amount
24
- currencyCode
25
- }
26
- totalShipping {
27
- amount
28
- currencyCode
17
+ itemCount
18
+ totals {
19
+ total {
20
+ amount
21
+ currencyCode
22
+ }
23
+ subtotal {
24
+ amount
25
+ currencyCode
26
+ }
27
+ totalShipping {
28
+ amount
29
+ currencyCode
30
+ }
29
31
  }
30
32
  shippingAddress {
31
33
  firstName
@@ -18,11 +18,11 @@ fragment CartLineFields on CartLine {
18
18
  productTitle
19
19
  productHandle
20
20
  productType
21
- merchandise {
21
+ variant {
22
22
  id
23
23
  title
24
- available
25
- quantityAvailable
24
+ isAvailable
25
+ availableStock
26
26
  price {
27
27
  amount
28
28
  currencyCode
@@ -38,15 +38,15 @@ fragment CartLineFields on CartLine {
38
38
  }
39
39
  }
40
40
  cost {
41
- amountPerQuantity {
41
+ pricePerUnit {
42
42
  amount
43
43
  currencyCode
44
44
  }
45
- totalAmount {
45
+ total {
46
46
  amount
47
47
  currencyCode
48
48
  }
49
- compareAtAmountPerQuantity {
49
+ compareAtPricePerUnit {
50
50
  amount
51
51
  currencyCode
52
52
  }
@@ -22,7 +22,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
22
22
  import { formatPrice } from "@doswiftly/storefront-sdk";
23
23
 
24
24
  export interface DiscountValidationResult {
25
- valid: boolean;
25
+ isValid: boolean;
26
26
  code?: string;
27
27
  discountType?: "PERCENTAGE" | "FIXED_AMOUNT" | "FREE_SHIPPING";
28
28
  discountValue?: number;
@@ -101,7 +101,7 @@ export function DiscountCodeInput({
101
101
  const result = await onValidate(trimmedCode);
102
102
  setValidationResult(result);
103
103
 
104
- if (!result.valid) {
104
+ if (!result.isValid) {
105
105
  setError(result.errorMessage || t("invalidCode"));
106
106
  }
107
107
  } catch (e: unknown) {
@@ -111,7 +111,7 @@ export function DiscountCodeInput({
111
111
  }
112
112
  } else {
113
113
  // No validation function, assume valid
114
- setValidationResult({ valid: true, code: trimmedCode });
114
+ setValidationResult({ isValid: true, code: trimmedCode });
115
115
  }
116
116
  }, [code, appliedCodes, onValidate]);
117
117
 
@@ -119,12 +119,12 @@ export function DiscountCodeInput({
119
119
  const trimmedCode = code.trim().toUpperCase();
120
120
 
121
121
  // If not yet validated, validate first
122
- if (!validationResult?.valid) {
122
+ if (!validationResult?.isValid) {
123
123
  await handleValidate();
124
124
  return;
125
125
  }
126
126
 
127
- if (validationResult.valid) {
127
+ if (validationResult.isValid) {
128
128
  await onApply(trimmedCode);
129
129
  // Reset state after successful apply
130
130
  setCode("");
@@ -148,7 +148,7 @@ export function DiscountCodeInput({
148
148
 
149
149
  // Format discount value for display
150
150
  const getDiscountPreview = () => {
151
- if (!validationResult?.valid) return null;
151
+ if (!validationResult?.isValid) return null;
152
152
 
153
153
  if (validationResult.estimatedSavings) {
154
154
  return t("savings", { savings: formatPrice(validationResult.estimatedSavings) });
@@ -185,7 +185,7 @@ export function DiscountCodeInput({
185
185
  disabled={disabled || isLoading}
186
186
  className={cn(
187
187
  "pl-9 pr-8 uppercase",
188
- validationResult?.valid && "border-green-500 focus-visible:ring-green-500",
188
+ validationResult?.isValid && "border-green-500 focus-visible:ring-green-500",
189
189
  error && "border-destructive focus-visible:ring-destructive"
190
190
  )}
191
191
  />
@@ -203,11 +203,11 @@ export function DiscountCodeInput({
203
203
  type="button"
204
204
  onClick={handleApply}
205
205
  disabled={disabled || isLoading || !code.trim()}
206
- variant={validationResult?.valid ? "default" : "secondary"}
206
+ variant={validationResult?.isValid ? "default" : "secondary"}
207
207
  >
208
208
  {isLoading ? (
209
209
  <Loader2 className="h-4 w-4 animate-spin" />
210
- ) : validationResult?.valid ? (
210
+ ) : validationResult?.isValid ? (
211
211
  <>
212
212
  <Check className="mr-2 h-4 w-4" />
213
213
  {tc("apply")}
@@ -219,7 +219,7 @@ export function DiscountCodeInput({
219
219
  </div>
220
220
 
221
221
  {/* Validation result - success preview */}
222
- {validationResult?.valid && (
222
+ {validationResult?.isValid && (
223
223
  <Alert className="border-green-500 bg-green-50 dark:bg-green-950/20">
224
224
  <Check className="h-4 w-4 text-green-600" />
225
225
  <AlertDescription className="text-green-700 dark:text-green-400">
@@ -24,7 +24,7 @@ type GiftCardStatus = "ACTIVE" | "USED" | "EXPIRED" | "DISABLED";
24
24
  * Gift card validation result
25
25
  */
26
26
  export interface GiftCardValidationResult {
27
- valid: boolean;
27
+ isValid: boolean;
28
28
  code: string;
29
29
  availableBalance?: {
30
30
  amount: string;
@@ -144,7 +144,7 @@ export function GiftCardInput({
144
144
  const result = await onValidate(code);
145
145
  setValidation(result);
146
146
 
147
- if (!result.valid && result.error) {
147
+ if (!result.isValid && result.error) {
148
148
  setError(result.error.message);
149
149
  }
150
150
  } catch (err: unknown) {
@@ -158,7 +158,7 @@ export function GiftCardInput({
158
158
  * Apply the validated gift card
159
159
  */
160
160
  const handleApply = useCallback(() => {
161
- if (validation?.valid) {
161
+ if (validation?.isValid) {
162
162
  onApply(code, validation);
163
163
  setCode("");
164
164
  setValidation(null);
@@ -172,7 +172,7 @@ export function GiftCardInput({
172
172
  (e: React.KeyboardEvent) => {
173
173
  if (e.key === "Enter") {
174
174
  e.preventDefault();
175
- if (validation?.valid) {
175
+ if (validation?.isValid) {
176
176
  handleApply();
177
177
  } else {
178
178
  handleValidate();
@@ -201,13 +201,13 @@ export function GiftCardInput({
201
201
  </div>
202
202
  <Button
203
203
  type="button"
204
- variant={validation?.valid ? "default" : "outline"}
205
- onClick={validation?.valid ? handleApply : handleValidate}
204
+ variant={validation?.isValid ? "default" : "outline"}
205
+ onClick={validation?.isValid ? handleApply : handleValidate}
206
206
  disabled={disabled || isValidating || !code}
207
207
  >
208
208
  {isValidating ? (
209
209
  <Loader2 className="h-4 w-4 animate-spin" />
210
- ) : validation?.valid ? (
210
+ ) : validation?.isValid ? (
211
211
  <>
212
212
  <Check className="mr-2 h-4 w-4" />
213
213
  {tc("apply")}
@@ -223,12 +223,12 @@ export function GiftCardInput({
223
223
  <div
224
224
  className={cn(
225
225
  "rounded-lg p-3 text-sm",
226
- validation.valid
226
+ validation.isValid
227
227
  ? "bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200"
228
228
  : "bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200"
229
229
  )}
230
230
  >
231
- {validation.valid ? (
231
+ {validation.isValid ? (
232
232
  <div className="flex items-center justify-between">
233
233
  <div className="flex items-center gap-2">
234
234
  <Check className="h-4 w-4" />
@@ -9,7 +9,7 @@ export interface DeliveryEstimateProps {
9
9
  latestDate?: string;
10
10
  address?: {
11
11
  city: string;
12
- province: string;
12
+ state: string;
13
13
  country: string;
14
14
  };
15
15
  className?: string;
@@ -97,7 +97,7 @@ export function DeliveryEstimate({
97
97
  <div className="flex items-start gap-2 text-sm text-muted-foreground">
98
98
  <MapPin className="h-4 w-4 mt-0.5 flex-shrink-0" />
99
99
  <span>
100
- Delivering to {address.city}, {address.province},{" "}
100
+ Delivering to {address.city}, {address.state},{" "}
101
101
  {address.country}
102
102
  </span>
103
103
  </div>
@@ -45,7 +45,7 @@ export function ProductCard({
45
45
  (tag) => tag.toLowerCase() === "new" || tag.toLowerCase() === "nowość"
46
46
  );
47
47
 
48
- const isOutOfStock = !product.availableForSale;
48
+ const isOutOfStock = !product.isAvailable;
49
49
  const isGiftCard = product.productType === "GIFT_CARD";
50
50
 
51
51
  return (
@@ -84,7 +84,7 @@ export function buildProductJsonLd(
84
84
  url: productUrl,
85
85
  priceCurrency: variant.price.currencyCode,
86
86
  price: variant.price.amount,
87
- availability: variant.availableForSale
87
+ availability: variant.isAvailable
88
88
  ? "https://schema.org/InStock"
89
89
  : "https://schema.org/OutOfStock",
90
90
  ...(variant.sku && { sku: variant.sku }),
@@ -1,42 +1,60 @@
1
1
  'use client';
2
2
 
3
3
  import { useEffect } from 'react';
4
- import { useRouter } from 'next/navigation';
5
4
  import { useAuthStore, useAuthHydrated } from '@doswiftly/storefront-sdk/react';
6
- import { createAuthTokenClient } from '@doswiftly/storefront-sdk';
7
-
8
- const { clearToken } = createAuthTokenClient();
9
5
 
10
6
  /**
11
- * Detects and fixes auth state desync between httpOnly cookie and Zustand store.
7
+ * Hydrate Zustand auth store z httpOnly cookie po mount klienta.
8
+ *
9
+ * Po Iteracji 2 (XSS fix) `accessToken` NIE persistuje w localStorage —
10
+ * token żyje tylko w-memory + httpOnly cookie (browser auto-sent w GraphQL
11
+ * requests). Po refresh page klient nie zna `customer` info bez round-trip.
12
12
  *
13
- * When server says authenticated (cookie exists initialIsAuthenticated: true)
14
- * but store has no accessToken after persist hydration (localStorage empty/cleared),
15
- * the client can't make authenticated GraphQL requests.
13
+ * Hook wywołuje `/api/auth/whoami` (BFF endpoint reads cookie server-side,
14
+ * forward Bearer do backend, return customer + isAuthenticated). Następnie
15
+ * setAuth() aktualizuje store. Auth-protected queries (`enabled: isAuthenticated`)
16
+ * unblockują się po hydration.
16
17
  *
17
- * Fix: clear the stale cookie, reset auth state, redirect to login.
18
- * The user re-authenticates once, which correctly populates both cookie AND localStorage.
18
+ * Token NIE jest body response kontynuuje życie w cookie. Klient operuje
19
+ * na samym `customer` info (display name, avatar etc.) i `isAuthenticated`
20
+ * gate; każdy GraphQL request automatycznie forward'uje cookie.
19
21
  */
20
22
  export function useAuthSync() {
21
- const router = useRouter();
22
- const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
23
- const accessToken = useAuthStore((s) => s.accessToken);
23
+ const setAuth = useAuthStore((s) => s.setAuth);
24
24
  const clearAuth = useAuthStore((s) => s.clearAuth);
25
+ const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
26
+ const customer = useAuthStore((s) => s.customer);
25
27
  const authHydrated = useAuthHydrated();
26
28
 
27
29
  useEffect(() => {
28
30
  if (!authHydrated) return;
29
31
 
30
- // Desync: cookie exists (isAuthenticated: true from server) but no token in store
31
- if (isAuthenticated && !accessToken) {
32
- // Reset store state immediately
33
- clearAuth();
34
- // Clear stale cookie THEN redirect — must await so proxy sees cleared cookie
35
- clearToken()
36
- .catch(() => {})
37
- .finally(() => {
38
- router.push('/auth/login');
39
- });
40
- }
41
- }, [authHydrated, isAuthenticated, accessToken, clearAuth, router]);
32
+ // Skip jeśli store już zhydrate'owany z customer info (po świeżym login flow
33
+ // setAuth populuje store; cookie + store w sync, no-op).
34
+ if (isAuthenticated && customer) return;
35
+
36
+ let cancelled = false;
37
+
38
+ void fetch('/api/auth/whoami', { credentials: 'include' })
39
+ .then((res) => res.json())
40
+ .then((data: { isAuthenticated: boolean; customer: { id: string; email: string; firstName?: string; lastName?: string; phone?: string } | null }) => {
41
+ if (cancelled) return;
42
+
43
+ if (data.isAuthenticated && data.customer) {
44
+ // Token nie wraca w body — store dostaje tylko customer info.
45
+ // Empty `''` accessToken sygnalizuje "auth via cookie" (in-memory state OK,
46
+ // GraphQL requests używają cookie auto-sent przez fetch credentials: include).
47
+ setAuth(data.customer, '');
48
+ } else {
49
+ clearAuth();
50
+ }
51
+ })
52
+ .catch(() => {
53
+ // Network error — leave store as-is, retry on next mount
54
+ });
55
+
56
+ return () => {
57
+ cancelled = true;
58
+ };
59
+ }, [authHydrated, isAuthenticated, customer, setAuth, clearAuth]);
42
60
  }
@@ -42,7 +42,7 @@ export function useCartActions() {
42
42
  ) => {
43
43
  await api.getState().addToCart([
44
44
  {
45
- merchandiseId: variantId,
45
+ variantId: variantId,
46
46
  quantity,
47
47
  ...(attributeSelections && attributeSelections.length > 0
48
48
  ? { attributeSelections }
@@ -9,7 +9,7 @@ import type { CartLineFields } from "@/lib/graphql/fragments";
9
9
  /**
10
10
  * Per-line attribute selection surfaced to cart UI (configurator choices).
11
11
  * Mirroruje CartLine.attributeSelections z GraphQL, ale ograniczony do pól
12
- * potrzebnych do wyświetlania — surchargeAmount kumulowany w cost.totalAmount
12
+ * potrzebnych do wyświetlania — surchargeAmount kumulowany w cost.total
13
13
  * (BUNDLED) lub emitowany jako child OrderItem po checkout (SEPARATE_LINE).
14
14
  */
15
15
  export interface CartItemAttributeSelection {
@@ -74,7 +74,7 @@ export function useCartSync() {
74
74
 
75
75
  // Map GraphQL lines to display-friendly items
76
76
  const items: CartItemData[] = (cart?.lines ?? []).map((line: CartLineFields) => {
77
- const merchandiseTitle = line.merchandise.title ?? "";
77
+ const merchandiseTitle = line.variant.title ?? "";
78
78
  // Hide generic variant names like "Default" or "Default Title"
79
79
  const isDefaultVariant = /^default(\s+title)?$/i.test(merchandiseTitle);
80
80
  const productTitle = line.productTitle || merchandiseTitle;
@@ -82,19 +82,19 @@ export function useCartSync() {
82
82
 
83
83
  return {
84
84
  lineId: line.id,
85
- variantId: line.merchandise.id,
86
- productId: line.productId || line.merchandise.id,
85
+ variantId: line.variant.id,
86
+ productId: line.productId || line.variant.id,
87
87
  productHandle: line.productHandle ?? undefined,
88
88
  productTitle,
89
89
  variantTitle,
90
90
  productType: line.productType ?? undefined,
91
91
  quantity: line.quantity,
92
92
  price: {
93
- amount: line.merchandise.price.amount,
94
- currencyCode: line.merchandise.price.currencyCode,
93
+ amount: line.variant.price.amount,
94
+ currencyCode: line.variant.price.currencyCode,
95
95
  },
96
- image: line.merchandise.image || null,
97
- available: line.merchandise.available,
96
+ image: line.variant.image || null,
97
+ available: line.variant.isAvailable,
98
98
  // Faza 1 — configurator selections (backend snapshot).
99
99
  attributeSelections: (line.attributeSelections ?? []).map((sel) => ({
100
100
  attributeDefinitionId: sel.attributeDefinitionId,
@@ -108,17 +108,17 @@ export function useCartSync() {
108
108
  });
109
109
 
110
110
  const totalQuantity = cart?.totalQuantity ?? 0;
111
- const subtotal = cart?.cost?.subtotalAmount
112
- ? parseFloat(cart.cost.subtotalAmount.amount)
111
+ const subtotal = cart?.cost?.subtotal
112
+ ? parseFloat(cart.cost.subtotal.amount)
113
113
  : 0;
114
- const total = cart?.cost?.totalAmount
115
- ? parseFloat(cart.cost.totalAmount.amount)
114
+ const total = cart?.cost?.total
115
+ ? parseFloat(cart.cost.total.amount)
116
116
  : subtotal;
117
- const currency = cart?.cost?.subtotalAmount?.currencyCode ?? "PLN";
117
+ const currency = cart?.cost?.subtotal?.currencyCode ?? "PLN";
118
118
 
119
119
  // Discount data from server
120
120
  const discountCodes: string[] = (cart?.discountCodes ?? [])
121
- .filter((dc) => dc.applicable)
121
+ .filter((dc) => dc.isApplicable)
122
122
  .map((dc) => dc.code);
123
123
  const totalDiscount = subtotal - total;
124
124