@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.
- package/dist/_internal/server/features/checkout/actions.js +11 -15
- package/dist/client/api/surface-error.js +9 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +5 -1
- package/dist/errors/error-handler.d.ts +6 -0
- package/dist/errors/error-handler.js +26 -28
- package/dist/features/account/hooks/useProfile.d.ts +8 -0
- package/dist/features/account/schemas/index.d.ts +12 -3
- package/dist/features/account/schemas/index.js +10 -3
- package/dist/features/auth/components/RegisterForm.js +2 -2
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/schemas/index.d.ts +8 -0
- package/dist/features/auth/schemas/index.js +15 -1
- package/dist/features/seller/actions/seller-actions.js +54 -13
- package/dist/features/seller/components/SellerOrdersView.js +24 -9
- package/dist/features/seller/components/SellerProductShell.js +17 -2
- package/dist/http/ApiClient.d.ts +11 -0
- package/dist/http/ApiClient.js +4 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
782
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
10
|
-
|
|
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:
|
|
37
|
-
code:
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 !==
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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",
|
package/dist/http/ApiClient.d.ts
CHANGED
|
@@ -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 {
|
package/dist/http/ApiClient.js
CHANGED
|
@@ -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 {
|