@mohasinac/appkit 3.3.1 → 3.3.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.
@@ -778,11 +778,15 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
778
778
  // decrement runs against `expansionPaid.decrements` lower down.
779
779
  const expansionPaid = getExpandedDecrements(cart.items);
780
780
  const productByIdPaid = new Map();
781
- for (const pid of expansionPaid.productIds) {
782
- const product = await unitOfWork.products.findById(pid);
781
+ // Independent reads across distinct product docs — batch them instead of
782
+ // awaiting one findById per product (mirrors the COD/UPI path above, which
783
+ // already batches its product lookups via Promise.all).
784
+ const fetchedProductsPaid = await Promise.all(expansionPaid.productIds.map((pid) => unitOfWork.products.findById(pid)));
785
+ expansionPaid.productIds.forEach((pid, i) => {
786
+ const product = fetchedProductsPaid[i];
783
787
  if (product)
784
788
  productByIdPaid.set(pid, product);
785
- }
789
+ });
786
790
  const productChecks = cart.items.map((item) => {
787
791
  const [firstMember] = getCartItemMemberIds(item);
788
792
  const product = productByIdPaid.get(firstMember) ?? null;
@@ -867,18 +871,10 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
867
871
  for (const { items: group, orderType } of orderGroups) {
868
872
  const firstItem = group[0].item;
869
873
  const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
870
- let shippingFee = 0;
871
- let storeOwnerId;
872
- const storeId = firstItem.storeId;
873
- if (storeId) {
874
- const store = await storeRepository.findById(storeId);
875
- storeOwnerId = store?.ownerId;
876
- const sellerUser = storeOwnerId ? await userRepository.findById(storeOwnerId) : null;
877
- const shippingConfig = sellerUser?.shippingConfig;
878
- if (shippingConfig?.isConfigured) {
879
- shippingFee = shippingConfig.customShippingPrice ?? 0;
880
- }
881
- }
874
+ // Reuses the same store/seller lookup as the COD/UPI path above instead
875
+ // of re-implementing it inline — was two sequential findById calls per
876
+ // seller group here, duplicated from resolveShippingCost.
877
+ const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId);
882
878
  let couponDiscount = 0;
883
879
  const appliedDiscounts = [];
884
880
  for (const coupon of appliedCoupons) {
@@ -1,9 +1,17 @@
1
1
  import { isApiError } from "./ApiError";
2
2
  import { normalizeError } from "../../errors/normalize";
3
3
  import { getErrorDisplay } from "../../errors/error-display-map";
4
+ /** True for `ApiError` instances and for any other thrown error (e.g. the
5
+ * lower-level `ApiClientError` from `ApiClient.request()`) that carries the
6
+ * same stable `code: string` field — both shapes route through the same
7
+ * toast-vs-inline-field-error logic below. */
8
+ function hasStableErrorCode(err) {
9
+ return (isApiError(err) ||
10
+ (err instanceof Error && typeof err.code === "string"));
11
+ }
4
12
  export function surfaceError(err, opts) {
5
13
  const { showToast, setFieldError, translate, report } = opts;
6
- if (isApiError(err)) {
14
+ if (hasStableErrorCode(err)) {
7
15
  const display = getErrorDisplay(err.code);
8
16
  const message = translate?.(display.messageKey) ?? err.message;
9
17
  if (display.field && setFieldError) {
package/dist/client.d.ts CHANGED
@@ -27,6 +27,7 @@ export type { GradientKey, RequiredThemeToken, ThemeMode, ThemeRecord, } from ".
27
27
  export { ConfirmDeleteModal } from "./ui/components/ConfirmDeleteModal";
28
28
  export { EmptyState } from "./ui/components/EmptyState";
29
29
  export { Skeleton } from "./ui/components/Skeleton";
30
+ export { Alert } from "./ui/components/Alert";
30
31
  export type { SkeletonProps } from "./ui/components/Skeleton";
31
32
  export { Divider } from "./ui/components/Divider";
32
33
  export { TabStrip } from "./ui/components/TabStrip";
@@ -79,6 +80,8 @@ export type { LotteryConfig, LotterySlot, ClientLotterySlot, ClientLotteryConfig
79
80
  export { BottomSheet } from "./features/layout/index";
80
81
  export { ImageUpload } from "./features/media/index";
81
82
  export type { ImageUploadProps } from "./features/media/index";
83
+ export { AvatarUpload } from "./features/media/index";
84
+ export type { AvatarUploadProps, ImageCropData } from "./features/media/index";
82
85
  export { ImageCropModal } from "./features/media/index";
83
86
  export { VideoTrimModal } from "./features/media/index";
84
87
  export { useMediaAbort } from "./features/media/index";
@@ -117,13 +120,15 @@ export type { OtpInputProps } from "./ui/components/OtpInput";
117
120
  export { DateInput, DateRangeInput } from "./ui/components/DateInput";
118
121
  export type { DateInputProps, DateRangeInputProps } from "./ui/components/DateInput";
119
122
  export type { FormShellProps, FormShellProviderProps, FormShellStep, FormShellContextValue, UseFormShellStateResult } from "./ui/forms";
120
- export { FormShell, FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./ui/forms";
123
+ export { FormShell, FormShellProvider, FormShellContext, useFormShell, useFormShellState, applyZodIssues } from "./ui/forms";
121
124
  export type { FieldInputProps } from "./ui/forms";
122
125
  export { FieldInput } from "./ui/forms";
123
126
  export type { FieldSelectProps } from "./ui/forms";
124
127
  export { FieldSelect } from "./ui/forms";
125
128
  export type { FieldCheckboxProps } from "./ui/forms";
126
129
  export { FieldCheckbox } from "./ui/forms";
130
+ export type { FieldTextareaProps } from "./ui/forms";
131
+ export { FieldTextarea } from "./ui/forms";
127
132
  export type { ColorPickerFieldProps } from "./ui/forms";
128
133
  export { ColorPickerField } from "./ui/forms";
129
134
  export { Select } from "./ui/components/Select";
@@ -171,6 +176,7 @@ export type { Address, AddressFormData } from "./features/account/index";
171
176
  export { AddressBook, AddressCard, AddressForm } from "./features/account/index";
172
177
  export type { AddressCardAddress, AddressCardProps } from "./features/account/index";
173
178
  export { useProfile, useUpdateProfile } from "./features/account/index";
179
+ export { updateProfileSchema } from "./features/account/index";
174
180
  export { CategoryProductsView } from "./features/categories/index";
175
181
  export type { CategoryItem } from "./features/categories/index";
176
182
  export { MediaImage } from "./features/media/index";
package/dist/client.js CHANGED
@@ -38,6 +38,7 @@ export { ConfirmDeleteModal } from "./ui/components/ConfirmDeleteModal";
38
38
  // S-STORE — pure-UI primitives + Seller view consumed by new dashboard pages
39
39
  export { EmptyState } from "./ui/components/EmptyState";
40
40
  export { Skeleton } from "./ui/components/Skeleton";
41
+ export { Alert } from "./ui/components/Alert";
41
42
  export { Divider } from "./ui/components/Divider";
42
43
  export { TabStrip } from "./ui/components/TabStrip";
43
44
  export { SellerProductsView } from "./features/seller/components/SellerProductsView";
@@ -131,6 +132,7 @@ export { PrizeDrawLotteryDetailView } from "./_internal/client/features/lottery/
131
132
  // BottomSheet - Shared export for bottom sheet.
132
133
  export { BottomSheet } from "./features/layout/index";
133
134
  export { ImageUpload } from "./features/media/index";
135
+ export { AvatarUpload } from "./features/media/index";
134
136
  // [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
135
137
  // ImageCropModal - Component for image crop modal.
136
138
  export { ImageCropModal } from "./features/media/index";
@@ -175,10 +177,11 @@ export { RadioItem } from "./ui/components/Radio";
175
177
  export { Input } from "./ui/components/Input";
176
178
  export { OtpInput } from "./ui/components/OtpInput";
177
179
  export { DateInput, DateRangeInput } from "./ui/components/DateInput";
178
- export { FormShell, FormShellProvider, FormShellContext, useFormShell, useFormShellState } from "./ui/forms";
180
+ export { FormShell, FormShellProvider, FormShellContext, useFormShell, useFormShellState, applyZodIssues } from "./ui/forms";
179
181
  export { FieldInput } from "./ui/forms";
180
182
  export { FieldSelect } from "./ui/forms";
181
183
  export { FieldCheckbox } from "./ui/forms";
184
+ export { FieldTextarea } from "./ui/forms";
182
185
  export { ColorPickerField } from "./ui/forms";
183
186
  export { Select } from "./ui/components/Select";
184
187
  export { Heading } from "./ui/components/Typography";
@@ -206,6 +209,7 @@ export { CartView, CartItemRow, CartSummary, CartDrawer, CheckoutView, CheckoutS
206
209
  export { useAddresses, useCreateAddress, useUpdateAddress, useDeleteAddress, useSetDefaultAddress, useAddress } from "./features/account/index";
207
210
  export { AddressBook, AddressCard, AddressForm } from "./features/account/index";
208
211
  export { useProfile, useUpdateProfile } from "./features/account/index";
212
+ export { updateProfileSchema } from "./features/account/index";
209
213
  export { CategoryProductsView } from "./features/categories/index";
210
214
  export { MediaImage } from "./features/media/index";
211
215
  export { StoreSidebar } from "./features/seller/components/SellerSidebar";
@@ -4,6 +4,12 @@ import { AppError } from "./base-error";
4
4
  /**
5
5
  * Handle API errors with consistent response format.
6
6
  * Use in Next.js API route catch blocks.
7
+ *
8
+ * Delegates classification to `mapToHttpError` — the same table `createRouteHandler`
9
+ * uses — so `DatabaseError`/bare Firestore error codes/`ApiError` are classified
10
+ * correctly here too, instead of silently falling through to a generic 500 the way
11
+ * this function previously did for anything that wasn't an `AppError` or a Zod-shaped
12
+ * object. Response shape is unchanged for existing callers.
7
13
  */
8
14
  export declare function handleApiError(error: unknown): NextResponse;
9
15
  /**
@@ -1,41 +1,39 @@
1
1
  import { NextResponse } from "next/server.js";
2
2
  import { AppError } from "./base-error";
3
- import { ERROR_CODES, ERROR_MESSAGES_BY_CODE } from "./error-codes";
3
+ import { mapToHttpError } from "./error-mapping";
4
4
  /**
5
5
  * Handle API errors with consistent response format.
6
6
  * Use in Next.js API route catch blocks.
7
+ *
8
+ * Delegates classification to `mapToHttpError` — the same table `createRouteHandler`
9
+ * uses — so `DatabaseError`/bare Firestore error codes/`ApiError` are classified
10
+ * correctly here too, instead of silently falling through to a generic 500 the way
11
+ * this function previously did for anything that wasn't an `AppError` or a Zod-shaped
12
+ * object. Response shape is unchanged for existing callers.
7
13
  */
8
14
  export function handleApiError(error) {
9
- if (error instanceof AppError) {
10
- if (error.statusCode >= 500) {
11
- console.error("[API Error]", {
12
- code: error.code,
13
- message: error.message,
14
- statusCode: error.statusCode,
15
- data: error.data,
16
- });
17
- }
18
- return NextResponse.json(error.toJSON(), { status: error.statusCode });
19
- }
20
- // Zod / schema validation errors
21
- if (error && typeof error === "object" && "issues" in error) {
22
- return NextResponse.json({
23
- success: false,
24
- error: "Validation failed",
25
- code: ERROR_CODES.VALIDATION_INVALID_INPUT,
26
- data: error,
27
- }, { status: 400 });
28
- }
29
- console.error("[Unexpected API Error]", {
30
- error: error instanceof Error
31
- ? { name: error.name, message: error.message, stack: error.stack }
32
- : error,
15
+ const mapped = mapToHttpError(error, {
16
+ isProduction: process.env.NODE_ENV === "production",
33
17
  });
18
+ if (mapped.status >= 500) {
19
+ console.error("[API Error]", {
20
+ code: mapped.code,
21
+ message: mapped.message,
22
+ statusCode: mapped.status,
23
+ data: error instanceof AppError ? error.data : undefined,
24
+ });
25
+ }
34
26
  return NextResponse.json({
35
27
  success: false,
36
- error: ERROR_MESSAGES_BY_CODE[ERROR_CODES.GEN_INTERNAL_ERROR],
37
- code: ERROR_CODES.GEN_INTERNAL_ERROR,
38
- }, { status: 500 });
28
+ error: mapped.message,
29
+ code: mapped.code,
30
+ statusCode: mapped.status,
31
+ ...(error instanceof AppError && error.data !== undefined
32
+ ? { data: error.data }
33
+ : mapped.issues
34
+ ? { data: { issues: mapped.issues } }
35
+ : {}),
36
+ }, { status: mapped.status });
39
37
  }
40
38
  /**
41
39
  * Log an error with optional context. Wraps console.error for package portability;
@@ -14,6 +14,14 @@ export interface UpdateCurrentProfileInput {
14
14
  displayName?: string;
15
15
  phoneNumber?: string;
16
16
  photoURL?: string;
17
+ avatarMetadata?: {
18
+ url: string;
19
+ position: {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ zoom: number;
24
+ };
17
25
  bio?: string;
18
26
  profileIsPublic?: boolean;
19
27
  }
@@ -183,16 +183,25 @@ export declare const userProfileSchema: z.ZodObject<{
183
183
  } | undefined;
184
184
  bio?: string | undefined;
185
185
  }>;
186
+ /**
187
+ * Client-side mirror of the server's updateProfileSchema
188
+ * (src/app/api/user/profile/route.ts) — kept field-for-field in sync so a
189
+ * value that passes client validation is guaranteed to pass server
190
+ * validation too.
191
+ */
186
192
  export declare const updateProfileSchema: z.ZodObject<{
187
193
  displayName: z.ZodOptional<z.ZodString>;
188
- phone: z.ZodOptional<z.ZodString>;
194
+ phoneNumber: z.ZodOptional<z.ZodString>;
189
195
  bio: z.ZodOptional<z.ZodString>;
196
+ profileIsPublic: z.ZodOptional<z.ZodBoolean>;
190
197
  }, "strip", z.ZodTypeAny, {
191
- phone?: string | undefined;
192
198
  displayName?: string | undefined;
199
+ phoneNumber?: string | undefined;
193
200
  bio?: string | undefined;
201
+ profileIsPublic?: boolean | undefined;
194
202
  }, {
195
- phone?: string | undefined;
196
203
  displayName?: string | undefined;
204
+ phoneNumber?: string | undefined;
197
205
  bio?: string | undefined;
206
+ profileIsPublic?: boolean | undefined;
198
207
  }>;
@@ -44,8 +44,15 @@ export const userProfileSchema = z.object({
44
44
  createdAt: z.string().optional(),
45
45
  updatedAt: z.string().optional(),
46
46
  });
47
+ /**
48
+ * Client-side mirror of the server's updateProfileSchema
49
+ * (src/app/api/user/profile/route.ts) — kept field-for-field in sync so a
50
+ * value that passes client validation is guaranteed to pass server
51
+ * validation too.
52
+ */
47
53
  export const updateProfileSchema = z.object({
48
- displayName: z.string().min(1).optional(),
49
- phone: z.string().optional(),
50
- bio: z.string().optional(),
54
+ displayName: z.string().min(1, "Enter your name").optional(),
55
+ phoneNumber: z.string().optional(),
56
+ bio: z.string().max(500, "Bio must be 500 characters or fewer").optional(),
57
+ profileIsPublic: z.boolean().optional(),
51
58
  });
@@ -8,10 +8,10 @@ import { Form } from "../../../ui/components/Form";
8
8
  import { FieldInput } from "../../../ui/forms/FieldInput";
9
9
  import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
10
10
  import { applyZodIssues } from "../../../ui/forms/FormShell";
11
- import { registerSchema } from "../schemas";
11
+ import { registerSchema, registerPasswordSchema } from "../schemas";
12
12
  const registerClientSchema = registerSchema.extend({
13
13
  displayName: z.string().min(1, "Enter your name"),
14
- confirmPassword: z.string().min(6),
14
+ confirmPassword: registerPasswordSchema,
15
15
  acceptTerms: z.literal(true, {
16
16
  errorMap: () => ({ message: "You must accept the terms to continue" }),
17
17
  }),
@@ -11,7 +11,7 @@
11
11
  * Permission naming: admin:resource:action
12
12
  * Ban action naming: plain verb phrase — write_reviews, place_bids, etc.
13
13
  */
14
- export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
14
+ export type Permission = "admin:dashboard:view" | "admin:users:read" | "admin:users:write" | "admin:users:delete" | "admin:user-bans:read" | "admin:user-bans:write" | "admin:products:read" | "admin:products:write" | "admin:products:delete" | "admin:orders:read" | "admin:orders:write" | "admin:returns:read" | "admin:returns:write" | "admin:stores:read" | "admin:stores:write" | "admin:store-addresses:read" | "admin:addresses:read" | "admin:addresses:write" | "admin:analytics:view" | "admin:payouts:read" | "admin:payouts:write" | "admin:categories:read" | "admin:categories:write" | "admin:categories:delete" | "admin:brands:read" | "admin:brands:write" | "admin:brands:delete" | "admin:coupons:read" | "admin:coupons:write" | "admin:coupons:delete" | "admin:deals:read" | "admin:deals:write" | "admin:featured:read" | "admin:featured:write" | "admin:reviews:read" | "admin:reviews:write" | "admin:reviews:delete" | "admin:blog:read" | "admin:blog:write" | "admin:blog:delete" | "admin:blog:publish" | "admin:bids:read" | "admin:bids:write" | "admin:media:read" | "admin:media:write" | "admin:media:delete" | "admin:site:read" | "admin:site:write" | "admin:settings:write" | "admin:navigation:read" | "admin:navigation:write" | "admin:sections:read" | "admin:sections:write" | "admin:carousel:read" | "admin:carousel:write" | "admin:carousel:delete" | "admin:ads:read" | "admin:ads:write" | "admin:ads:delete" | "admin:faqs:read" | "admin:faqs:write" | "admin:faqs:delete" | "admin:newsletter:read" | "admin:newsletter:write" | "admin:contact:read" | "admin:events:read" | "admin:events:write" | "admin:events:delete" | "admin:event-entries:read" | "admin:event-entries:write" | "admin:support-tickets:read" | "admin:support-tickets:write" | "admin:support-tickets:assign" | "admin:support-tickets:close" | "admin:scammers:read" | "admin:scammers:write" | "admin:scammers:verify" | "admin:scammers:delete" | "admin:sessions:read" | "admin:sessions:delete" | "admin:notifications:read" | "admin:notifications:write" | "admin:carts:read" | "admin:wishlists:read" | "admin:feature-flags:read" | "admin:feature-flags:write" | "admin:copilot:view" | "admin:team:read" | "admin:team:write" | "admin:maintenance:view-server-errors" | "admin:maintenance:view-client-errors" | "admin:maintenance:view-function-errors" | "admin:maintenance:view-cloud-logs" | "admin:maintenance:view-payment-rollbacks" | "admin:maintenance:run-analysis" | "admin:maintenance:purge-errors";
15
15
  /**
16
16
  * Granular user actions that can be soft-banned individually.
17
17
  * A user may have multiple simultaneous soft bans of different types.
@@ -17,6 +17,14 @@ export declare const loginSchema: z.ZodObject<{
17
17
  password: string;
18
18
  email: string;
19
19
  }>;
20
+ /**
21
+ * Matches the server-side complexity rule enforced in
22
+ * src/app/api/auth/register/route.ts — kept in sync so a password that fails
23
+ * client-side validation is exactly the same one that would fail server-side,
24
+ * instead of passing the client gate and failing with a generic toast after
25
+ * the network round-trip.
26
+ */
27
+ export declare const registerPasswordSchema: z.ZodString;
20
28
  export declare const registerSchema: z.ZodObject<{
21
29
  email: z.ZodString;
22
30
  password: z.ZodString;
@@ -1,5 +1,6 @@
1
1
  export * from "./firestore";
2
2
  import { z } from "zod";
3
+ import { ERROR_MESSAGES } from "../../../errors/messages";
3
4
  // --- Form schemas -------------------------------------------------------------
4
5
  /**
5
6
  * Login form schema — use with react-hook-form + zodResolver.
@@ -12,9 +13,22 @@ export const loginSchema = z.object({
12
13
  email: z.string().email(),
13
14
  password: z.string().min(6),
14
15
  });
16
+ /**
17
+ * Matches the server-side complexity rule enforced in
18
+ * src/app/api/auth/register/route.ts — kept in sync so a password that fails
19
+ * client-side validation is exactly the same one that would fail server-side,
20
+ * instead of passing the client gate and failing with a generic toast after
21
+ * the network round-trip.
22
+ */
23
+ export const registerPasswordSchema = z
24
+ .string()
25
+ .min(8, ERROR_MESSAGES.PASSWORD.TOO_SHORT)
26
+ .regex(/[A-Z]/, ERROR_MESSAGES.PASSWORD.NO_UPPERCASE)
27
+ .regex(/[a-z]/, ERROR_MESSAGES.PASSWORD.NO_LOWERCASE)
28
+ .regex(/[0-9]/, ERROR_MESSAGES.PASSWORD.NO_NUMBER);
15
29
  export const registerSchema = z.object({
16
30
  email: z.string().email(),
17
- password: z.string().min(6),
31
+ password: registerPasswordSchema,
18
32
  displayName: z.string().min(1).optional(),
19
33
  });
20
34
  export const forgotPasswordSchema = z.object({
@@ -224,6 +224,9 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
224
224
  if (!userDoc.payoutDetails?.isConfigured) {
225
225
  throw new ValidationError("Payout details are not set up. Please configure your payout method before requesting a payout.");
226
226
  }
227
+ // order.storeId is the store slug, not the seller's Firebase UID — resolve
228
+ // the caller's store once so eligibility can compare like-for-like.
229
+ const callerStore = userRole !== "admin" ? await storeRepository.findByOwnerId(userId) : null;
227
230
  const orders = await Promise.all(orderIds.map((id) => orderRepository.findById(id)));
228
231
  const requested = [];
229
232
  const skipped = [];
@@ -235,7 +238,7 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
235
238
  skipped.push(id);
236
239
  continue;
237
240
  }
238
- if (userRole !== "admin" && order.storeId !== userId) {
241
+ if (userRole !== "admin" && order.storeId !== callerStore?.id) {
239
242
  skipped.push(id);
240
243
  continue;
241
244
  }
@@ -317,9 +320,15 @@ export async function createSellerProduct(userId, userName, userEmail, input) {
317
320
  sellerId: userId,
318
321
  sellerName: userName,
319
322
  sellerEmail: userEmail,
320
- status: "draft",
323
+ // Pass through the caller's intended status (draft vs published) instead
324
+ // of hardcoding "draft" — this previously made every "Publish" click
325
+ // silently save as a draft regardless of what the wizard sent.
326
+ status: finalizedData.status ?? "draft",
327
+ });
328
+ serverLogger.info("createSellerProduct: product created", {
329
+ userId,
330
+ status: finalizedData.status ?? "draft",
321
331
  });
322
- serverLogger.info("createSellerProduct: product created", { userId });
323
332
  }
324
333
  // --- Read Actions -------------------------------------------------------------
325
334
  export async function getSellerStore(userId) {
@@ -342,7 +351,20 @@ export async function getSellerPayoutSettings(userId) {
342
351
  return { ...details, bankAccount: safeBank };
343
352
  }
344
353
  export async function listSellerOrders(userId, params) {
345
- const sellerProducts = await productRepository.findByStore(userId);
354
+ // productRepository.findByStore expects the store slug, not the seller's
355
+ // Firebase UID — resolve the caller's store first.
356
+ const store = await storeRepository.findByOwnerId(userId);
357
+ if (!store) {
358
+ return {
359
+ items: [],
360
+ total: 0,
361
+ page: 1,
362
+ pageSize: params?.pageSize ?? 20,
363
+ totalPages: 0,
364
+ hasMore: false,
365
+ };
366
+ }
367
+ const sellerProducts = await productRepository.findByStore(store.id);
346
368
  const productIds = sellerProducts.map((p) => p.id);
347
369
  if (productIds.length === 0) {
348
370
  return {
@@ -362,7 +384,10 @@ export async function listSellerOrders(userId, params) {
362
384
  });
363
385
  }
364
386
  export async function getSellerAnalytics(userId) {
365
- const products = await productRepository.findByStore(userId);
387
+ // productRepository.findByStore expects the store slug, not the seller's
388
+ // Firebase UID — resolve the caller's store first.
389
+ const store = await storeRepository.findByOwnerId(userId);
390
+ const products = store ? await productRepository.findByStore(store.id) : [];
366
391
  const productIds = products.map((p) => p.id);
367
392
  let allOrders = [];
368
393
  if (productIds.length > 0) {
@@ -449,8 +474,12 @@ export async function sellerUpdateProduct(userId, userRole, productId, input) {
449
474
  const existing = await productRepository.findById(productId);
450
475
  if (!existing)
451
476
  throw new NotFoundError("Product not found");
452
- if (userRole !== "admin" && existing.storeId !== userId)
453
- throw new AuthorizationError("You do not own this product");
477
+ // existing.storeId is the store slug, not the seller's Firebase UID.
478
+ if (userRole !== "admin") {
479
+ const store = await storeRepository.findByOwnerId(userId);
480
+ if (!store || existing.storeId !== store.id)
481
+ throw new AuthorizationError("You do not own this product");
482
+ }
454
483
  const finalizedData = await finalizeProductMediaReferences(input);
455
484
  const updated = await productRepository.updateProduct(productId, finalizedData);
456
485
  serverLogger.info("sellerUpdateProduct", { userId, productId });
@@ -460,8 +489,12 @@ export async function sellerDeleteProduct(userId, userRole, productId) {
460
489
  const existing = await productRepository.findById(productId);
461
490
  if (!existing)
462
491
  throw new NotFoundError("Product not found");
463
- if (userRole !== "admin" && existing.storeId !== userId)
464
- throw new AuthorizationError("You do not own this product");
492
+ // existing.storeId is the store slug, not the seller's Firebase UID.
493
+ if (userRole !== "admin") {
494
+ const store = await storeRepository.findByOwnerId(userId);
495
+ if (!store || existing.storeId !== store.id)
496
+ throw new AuthorizationError("You do not own this product");
497
+ }
465
498
  await productRepository.delete(productId);
466
499
  serverLogger.info("sellerDeleteProduct", { userId, productId });
467
500
  }
@@ -487,8 +520,12 @@ export async function customShipOrder(userId, userRole, orderId, input) {
487
520
  const order = await orderRepository.findById(orderId);
488
521
  if (!order)
489
522
  throw new NotFoundError("Order not found");
490
- if (userRole !== "admin" && order.storeId !== userId)
491
- throw new AuthorizationError("You do not own this order");
523
+ // order.storeId is the store slug, not the seller's Firebase UID.
524
+ if (userRole !== "admin") {
525
+ const store = await storeRepository.findByOwnerId(userId);
526
+ if (!store || order.storeId !== store.id)
527
+ throw new AuthorizationError("You do not own this order");
528
+ }
492
529
  if (order.status === OrderStatusValues.SHIPPED ||
493
530
  order.status === OrderStatusValues.DELIVERED)
494
531
  throw new ValidationError("Order is already shipped");
@@ -523,8 +560,12 @@ export async function markEmiInstallmentPaid(userId, userRole, orderId, input) {
523
560
  const order = await orderRepository.findById(orderId);
524
561
  if (!order)
525
562
  throw new NotFoundError("Order not found");
526
- if (userRole !== "admin" && order.storeId !== userId)
527
- throw new AuthorizationError("You do not own this order");
563
+ // order.storeId is the store slug, not the seller's Firebase UID.
564
+ if (userRole !== "admin") {
565
+ const store = await storeRepository.findByOwnerId(userId);
566
+ if (!store || order.storeId !== store.id)
567
+ throw new AuthorizationError("You do not own this order");
568
+ }
528
569
  if (!order.emiEnabled)
529
570
  throw new ValidationError("This order is not on an EMI plan");
530
571
  const installments = order.emiInstallments ?? [];
@@ -275,20 +275,35 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
275
275
  render: (row) => _jsx(Span, { size: "xs", color: "muted", children: row.updatedAt }),
276
276
  },
277
277
  ];
278
+ const [shippingRowId, setShippingRowId] = useState(null);
278
279
  const handleQuickShip = useCallback(async (row, e) => {
279
280
  e.stopPropagation();
280
- const res = await fetch(`${orderDetailApiBase}/${row.id}`, {
281
- method: "PATCH",
282
- headers: { "Content-Type": "application/json" },
283
- body: JSON.stringify({ status: "shipped" }),
284
- }).catch(() => null);
285
- if (res?.ok)
281
+ setShippingRowId(row.id);
282
+ try {
283
+ const res = await fetch(`${orderDetailApiBase}/${row.id}`, {
284
+ method: "PATCH",
285
+ headers: { "Content-Type": "application/json" },
286
+ body: JSON.stringify({ status: "shipped" }),
287
+ });
288
+ if (!res.ok) {
289
+ const body = await res.json().catch(() => null);
290
+ throw new Error(body?.error ?? "Failed to mark order shipped");
291
+ }
292
+ showToast("Order marked shipped.", "success");
286
293
  setSelectedOrderId(null);
287
- }, [orderDetailApiBase]);
294
+ }
295
+ catch (err) {
296
+ void normalizeError(err);
297
+ showToast(err instanceof Error ? err.message : "Failed to mark order shipped.", "error");
298
+ }
299
+ finally {
300
+ setShippingRowId(null);
301
+ }
302
+ }, [orderDetailApiBase, showToast]);
288
303
  const renderRowActions = useCallback((row) => {
289
304
  const isShippable = ["PENDING", "PROCESSING", "CONFIRMED"].includes(row.status?.toUpperCase() ?? "");
290
- return (_jsxs(Row, { align: "center", gap: "xs", children: [isShippable && (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => void handleQuickShip(row, e), "aria-label": "Mark as shipped", title: "Mark shipped", children: _jsx(Truck, { className: "h-4 w-4" }) })), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); setSelectedOrderId(row.id); }, title: "View order details", "aria-label": "View order details", children: _jsx(Eye, { className: "h-4 w-4" }) })] }));
291
- }, [handleQuickShip]);
305
+ return (_jsxs(Row, { align: "center", gap: "xs", children: [isShippable && (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => void handleQuickShip(row, e), "aria-label": "Mark as shipped", title: "Mark shipped", isLoading: shippingRowId === row.id, disabled: shippingRowId !== null, children: _jsx(Truck, { className: "h-4 w-4" }) })), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); setSelectedOrderId(row.id); }, title: "View order details", "aria-label": "View order details", children: _jsx(Eye, { className: "h-4 w-4" }) })] }));
306
+ }, [handleQuickShip, shippingRowId]);
292
307
  const selection = useBulkSelection({ items: rows, keyExtractor: (r) => r.id });
293
308
  const handlePrintPackingSlips = useCallback(() => {
294
309
  const ids = selection.selectedIds.join(",");
@@ -296,17 +296,32 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
296
296
  {
297
297
  label: "Basic",
298
298
  render: ({ values, onChange }) => (_jsx(StepBasic, { values: values, onChange: onChange, renderCategorySelector: renderCategorySelector, renderBrandSelector: renderBrandSelector, renderTemplateSelector: renderTemplateSelector })),
299
- validate: (v) => (!v.title?.trim() ? "Title is required" : null),
299
+ validate: (v) => {
300
+ if (!v.title?.trim() || v.title.trim().length < 3)
301
+ return "Title must be at least 3 characters";
302
+ if (!v.description?.trim() || v.description.trim().length < 20)
303
+ return "Description must be at least 20 characters";
304
+ return null;
305
+ },
300
306
  },
301
307
  {
302
308
  label: "Media",
303
309
  render: ({ values, onChange }) => (_jsx(StepMedia, { values: values, onChange: onChange, storeSlug: storeSlug })),
310
+ validate: (v) => (!v.mainImage ? "A main image is required" : null),
304
311
  },
305
312
  ...(typeSpecificStep ? [typeSpecificStep] : []),
306
313
  {
307
314
  label: "Pricing",
308
315
  render: ({ values, onChange }) => (_jsx(StepPricing, { values: values, onChange: onChange, listingType: listingType })),
309
- validate: (v) => (!v.price ? "Price is required" : null),
316
+ validate: (v) => {
317
+ if (!v.price)
318
+ return "Price is required";
319
+ if (pluginForMode(listingType).showsStockQuantity &&
320
+ (v.stockQuantity === undefined || v.stockQuantity === null)) {
321
+ return "Stock quantity is required";
322
+ }
323
+ return null;
324
+ },
310
325
  },
311
326
  {
312
327
  label: "Shipping",
@@ -20,6 +20,17 @@ import type { JsonValue } from "../schemas/types";
20
20
  export declare class ApiClientError extends Error {
21
21
  readonly status: number;
22
22
  readonly data?: unknown | undefined;
23
+ /** Stable server error code, e.g. HTTP_ERROR_CODES.VALIDATION_FAILED — lets
24
+ * `surfaceError`/`ERROR_DISPLAY_MAP` route this to a toast or an inline
25
+ * field error the same way a `client/api/ApiError` would. */
26
+ readonly code?: string;
27
+ /** Serialised Zod issues, when the failure was a validation error. */
28
+ readonly issues?: {
29
+ message: string;
30
+ path?: (string | number)[];
31
+ code?: string;
32
+ }[];
33
+ readonly requestId?: string;
23
34
  constructor(message: string, status: number, data?: unknown | undefined);
24
35
  }
25
36
  export interface RequestConfig extends RequestInit {
@@ -22,6 +22,10 @@ export class ApiClientError extends Error {
22
22
  this.status = status;
23
23
  this.data = data;
24
24
  this.name = "ApiClientError";
25
+ const body = data;
26
+ this.code = typeof body?.code === "string" ? body.code : undefined;
27
+ this.issues = Array.isArray(body?.issues) ? body.issues : undefined;
28
+ this.requestId = typeof body?.requestId === "string" ? body.requestId : undefined;
25
29
  }
26
30
  }
27
31
  export class ApiClient {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"