@mohasinac/appkit 3.2.6 → 3.2.8
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/shared/actions/action-registry.js +62 -0
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +4 -0
- package/dist/features/about/components/PublicProfileView.js +8 -5
- package/dist/features/addresses/repository/addresses.repository.d.ts +14 -1
- package/dist/features/addresses/repository/addresses.repository.js +122 -1
- package/dist/features/addresses/schemas/firestore.d.ts +22 -3
- package/dist/features/addresses/schemas/firestore.js +25 -0
- package/dist/features/addresses/schemas/index.d.ts +2 -2
- package/dist/features/admin/components/AdminAddressClustersView.d.ts +5 -0
- package/dist/features/admin/components/AdminAddressClustersView.js +38 -0
- package/dist/features/admin/components/AdminAddressesView.d.ts +5 -0
- package/dist/features/admin/components/AdminAddressesView.js +137 -0
- package/dist/features/admin/components/AdminPaymentClustersView.d.ts +5 -0
- package/dist/features/admin/components/AdminPaymentClustersView.js +37 -0
- package/dist/features/admin/components/AdminPaymentMethodsView.d.ts +5 -0
- package/dist/features/admin/components/AdminPaymentMethodsView.js +130 -0
- package/dist/features/admin/components/index.d.ts +8 -0
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/auth/actions/profile-actions.d.ts +3 -1
- package/dist/features/auth/actions/profile-actions.js +29 -2
- package/dist/features/cart/schemas/index.d.ts +6 -6
- package/dist/features/orders/schemas/index.d.ts +4 -4
- package/dist/features/payments/repository/saved-payment-methods.repository.d.ts +48 -0
- package/dist/features/payments/repository/saved-payment-methods.repository.js +236 -0
- package/dist/features/payments/schemas/index.d.ts +1 -0
- package/dist/features/payments/schemas/index.js +1 -0
- package/dist/features/payments/schemas/saved-methods-firestore.d.ts +63 -0
- package/dist/features/payments/schemas/saved-methods-firestore.js +60 -0
- package/dist/features/payments/server.d.ts +2 -0
- package/dist/features/payments/server.js +2 -0
- package/dist/features/promotions/schemas/index.d.ts +6 -6
- package/dist/features/reviews/repository/reviews.repository.d.ts +12 -0
- package/dist/features/reviews/repository/reviews.repository.js +24 -0
- package/dist/features/reviews/schemas/firestore.d.ts +5 -1
- package/dist/features/reviews/schemas/firestore.js +2 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -6
- package/dist/features/seller/components/SellerAddressesView.js +7 -3
- package/dist/features/support/schemas/index.d.ts +8 -8
- package/dist/features/wishlist/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/next/routing/route-map.d.ts +8 -0
- package/dist/next/routing/route-map.js +4 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +66 -66
- package/dist/schemas/webhooks/razorpay.d.ts +50 -50
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +2 -0
- package/dist/security/pii-schemas.js +2 -0
- package/package.json +1 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SavedPaymentMethodsRepository
|
|
3
|
+
*
|
|
4
|
+
* Persists user payment identifiers (UPI VPAs, card tokens, etc.) for
|
|
5
|
+
* checkout pre-fill and cross-account fraud detection.
|
|
6
|
+
*
|
|
7
|
+
* PII: `identifier` is encrypted at rest (AES-256-GCM) — never returned
|
|
8
|
+
* to clients. `identifierHash` is unencrypted SHA-256 for cross-account
|
|
9
|
+
* dedup queries. `displayLabel` is pre-masked and safe to display.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
13
|
+
import { DatabaseError } from "../../../errors";
|
|
14
|
+
import { serverLogger } from "../../../monitoring";
|
|
15
|
+
import { BaseRepository, prepareForFirestore, } from "../../../providers/db-firebase";
|
|
16
|
+
import { PAYMENT_METHOD_PII_FIELDS, decryptPiiFields, encryptPiiFields, } from "../../../security";
|
|
17
|
+
import { SAVED_PAYMENT_METHOD_FIELDS, SAVED_PAYMENT_METHODS_COLLECTION, } from "../schemas/saved-methods-firestore";
|
|
18
|
+
function normaliseIdentifier(s) {
|
|
19
|
+
return s.toLowerCase().replace(/\s+/g, "").trim();
|
|
20
|
+
}
|
|
21
|
+
function computeIdentifierHash(type, identifier) {
|
|
22
|
+
return createHash("sha256")
|
|
23
|
+
.update(`${type}|${normaliseIdentifier(identifier)}`)
|
|
24
|
+
.digest("hex");
|
|
25
|
+
}
|
|
26
|
+
export class SavedPaymentMethodsRepository extends BaseRepository {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(SAVED_PAYMENT_METHODS_COLLECTION);
|
|
29
|
+
}
|
|
30
|
+
decrypt(doc) {
|
|
31
|
+
return decryptPiiFields(doc, [...PAYMENT_METHOD_PII_FIELDS]);
|
|
32
|
+
}
|
|
33
|
+
encrypt(data) {
|
|
34
|
+
return encryptPiiFields(data, [...PAYMENT_METHOD_PII_FIELDS]);
|
|
35
|
+
}
|
|
36
|
+
mapDoc(snap) {
|
|
37
|
+
const raw = super.mapDoc(snap);
|
|
38
|
+
return this.decrypt(raw);
|
|
39
|
+
}
|
|
40
|
+
async createWithId(id, data) {
|
|
41
|
+
return super.createWithId(id, this.encrypt(data));
|
|
42
|
+
}
|
|
43
|
+
async update(id, data) {
|
|
44
|
+
return super.update(id, this.encrypt(data));
|
|
45
|
+
}
|
|
46
|
+
/** List all saved methods for a user. Never returns raw `identifier` — only `displayLabel`. */
|
|
47
|
+
async listByUser(userId) {
|
|
48
|
+
try {
|
|
49
|
+
const snap = await this.getCollection()
|
|
50
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
|
|
51
|
+
.orderBy(SAVED_PAYMENT_METHOD_FIELDS.LAST_USED_AT, "desc")
|
|
52
|
+
.get();
|
|
53
|
+
return snap.docs.map((d) => {
|
|
54
|
+
const doc = this.mapDoc(d);
|
|
55
|
+
doc.identifier = ""; // never expose decrypted PII to caller
|
|
56
|
+
return doc;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
void normalizeError(error);
|
|
61
|
+
throw new DatabaseError(`Failed to list payment methods for user:${userId}`, error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Idempotent upsert by (userId + identifierHash).
|
|
66
|
+
* Computes hash + encrypts identifier before write.
|
|
67
|
+
* Updates lastUsedAt on re-add.
|
|
68
|
+
*/
|
|
69
|
+
async upsertForUser(userId, input) {
|
|
70
|
+
try {
|
|
71
|
+
const hash = computeIdentifierHash(input.type, input.identifier);
|
|
72
|
+
// Check for existing
|
|
73
|
+
const existing = await this.getCollection()
|
|
74
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
|
|
75
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.IDENTIFIER_HASH, "==", hash)
|
|
76
|
+
.limit(1)
|
|
77
|
+
.get();
|
|
78
|
+
const now = new Date();
|
|
79
|
+
if (!existing.empty) {
|
|
80
|
+
const docId = existing.docs[0].id;
|
|
81
|
+
return this.update(docId, { lastUsedAt: now, displayLabel: input.displayLabel });
|
|
82
|
+
}
|
|
83
|
+
const docRef = this.getCollection().doc();
|
|
84
|
+
const data = {
|
|
85
|
+
userId,
|
|
86
|
+
type: input.type,
|
|
87
|
+
identifier: input.identifier,
|
|
88
|
+
displayLabel: input.displayLabel,
|
|
89
|
+
identifierHash: hash,
|
|
90
|
+
isDefault: input.isDefault ?? false,
|
|
91
|
+
lastUsedAt: now,
|
|
92
|
+
createdAt: now,
|
|
93
|
+
updatedAt: now,
|
|
94
|
+
};
|
|
95
|
+
await docRef.set(prepareForFirestore(this.encrypt(data)));
|
|
96
|
+
serverLogger.info("Saved payment method created", { userId, type: input.type, docId: docRef.id });
|
|
97
|
+
const refetched = await this.findById(docRef.id);
|
|
98
|
+
if (!refetched)
|
|
99
|
+
throw new DatabaseError("Payment method not readable after create");
|
|
100
|
+
refetched.identifier = "";
|
|
101
|
+
return refetched;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
void normalizeError(error);
|
|
105
|
+
throw new DatabaseError(`Failed to upsert payment method for user:${userId}`, error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Cross-account lookup by identifierHash. Returns display-safe docs (identifier stripped). */
|
|
109
|
+
async listByIdentifierHash(hash) {
|
|
110
|
+
try {
|
|
111
|
+
const snap = await this.getCollection()
|
|
112
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.IDENTIFIER_HASH, "==", hash)
|
|
113
|
+
.get();
|
|
114
|
+
return snap.docs.map((d) => {
|
|
115
|
+
const doc = this.mapDoc(d);
|
|
116
|
+
doc.identifier = "";
|
|
117
|
+
return doc;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
void normalizeError(error);
|
|
122
|
+
throw new DatabaseError(`Failed to lookup payment methods by hash`, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Batch-ban all methods for a user. Used by hard-ban cascade. */
|
|
126
|
+
async banAllForUser(userId, banData) {
|
|
127
|
+
try {
|
|
128
|
+
const snap = await this.getCollection()
|
|
129
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
|
|
130
|
+
.get();
|
|
131
|
+
if (snap.empty)
|
|
132
|
+
return 0;
|
|
133
|
+
const now = new Date();
|
|
134
|
+
const batch = this.db.batch();
|
|
135
|
+
snap.docs.forEach((doc) => {
|
|
136
|
+
batch.update(doc.ref, {
|
|
137
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS]: "banned",
|
|
138
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BAN_REASON]: banData.banReason,
|
|
139
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BANNED_BY]: banData.bannedBy,
|
|
140
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT]: now,
|
|
141
|
+
[SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED]: true,
|
|
142
|
+
[SAVED_PAYMENT_METHOD_FIELDS.UPDATED_AT]: now,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
await batch.commit();
|
|
146
|
+
return snap.size;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
void normalizeError(error);
|
|
150
|
+
throw new DatabaseError(`Failed to ban payment methods for user:${userId}`, error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Reverse auto-ban cascade on user unban. Leaves manually-banned methods untouched. */
|
|
154
|
+
async unbanAutoForUser(userId) {
|
|
155
|
+
try {
|
|
156
|
+
const snap = await this.getCollection()
|
|
157
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
|
|
158
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED, "==", true)
|
|
159
|
+
.get();
|
|
160
|
+
if (snap.empty)
|
|
161
|
+
return 0;
|
|
162
|
+
const now = new Date();
|
|
163
|
+
const batch = this.db.batch();
|
|
164
|
+
snap.docs.forEach((doc) => {
|
|
165
|
+
batch.update(doc.ref, {
|
|
166
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS]: null,
|
|
167
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BAN_REASON]: null,
|
|
168
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BANNED_BY]: null,
|
|
169
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT]: null,
|
|
170
|
+
[SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED]: null,
|
|
171
|
+
[SAVED_PAYMENT_METHOD_FIELDS.UPDATED_AT]: now,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
await batch.commit();
|
|
175
|
+
return snap.size;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
void normalizeError(error);
|
|
179
|
+
throw new DatabaseError(`Failed to unban payment methods for user:${userId}`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** List by banStatus for admin view. Returns display-safe docs. */
|
|
183
|
+
async listByBanStatus(banStatus, limit = 50, offset = 0) {
|
|
184
|
+
try {
|
|
185
|
+
const snap = await this.getCollection()
|
|
186
|
+
.where(SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS, "==", banStatus)
|
|
187
|
+
.orderBy(SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT, "desc")
|
|
188
|
+
.limit(limit)
|
|
189
|
+
.offset(offset)
|
|
190
|
+
.get();
|
|
191
|
+
return snap.docs.map((d) => {
|
|
192
|
+
const doc = this.mapDoc(d);
|
|
193
|
+
doc.identifier = "";
|
|
194
|
+
return doc;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
void normalizeError(error);
|
|
199
|
+
throw new DatabaseError(`Failed to list payment methods by banStatus:${banStatus}`, error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async banById(id, banData) {
|
|
203
|
+
await this.update(id, {
|
|
204
|
+
banStatus: "banned",
|
|
205
|
+
banReason: banData.banReason,
|
|
206
|
+
bannedBy: banData.bannedBy,
|
|
207
|
+
bannedAt: new Date(),
|
|
208
|
+
autoBanned: false,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async clearBanById(id) {
|
|
212
|
+
const now = new Date();
|
|
213
|
+
await this.db.collection(SAVED_PAYMENT_METHODS_COLLECTION).doc(id).update({
|
|
214
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS]: null,
|
|
215
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BAN_REASON]: null,
|
|
216
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BANNED_BY]: null,
|
|
217
|
+
[SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT]: null,
|
|
218
|
+
[SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED]: null,
|
|
219
|
+
[SAVED_PAYMENT_METHOD_FIELDS.UNBAN_REQUEST_NOTE]: null,
|
|
220
|
+
[SAVED_PAYMENT_METHOD_FIELDS.UNBAN_REQUESTED_AT]: null,
|
|
221
|
+
[SAVED_PAYMENT_METHOD_FIELDS.UPDATED_AT]: now,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async deleteForUser(userId, id) {
|
|
225
|
+
const existing = await this.findById(id);
|
|
226
|
+
if (!existing || existing.userId !== userId) {
|
|
227
|
+
throw new DatabaseError(`Payment method ${id} not found for user:${userId}`);
|
|
228
|
+
}
|
|
229
|
+
await this.delete(id);
|
|
230
|
+
}
|
|
231
|
+
/** Public wrapper — compute identifier hash from outside the class (e.g. in API routes). */
|
|
232
|
+
computeIdentifierHash(type, identifier) {
|
|
233
|
+
return computeIdentifierHash(type, identifier);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export const savedPaymentMethodsRepository = new SavedPaymentMethodsRepository();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./firestore";
|
|
2
|
+
export * from "./saved-methods-firestore";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
export declare const payoutStatusEnumSchema: z.ZodEnum<["pending", "processing", "completed", "failed"]>;
|
|
4
5
|
export declare const payoutPaymentMethodSchema: z.ZodEnum<["bank_transfer", "upi"]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "./firestore";
|
|
2
|
+
export * from "./saved-methods-firestore";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
4
5
|
import { auditTimestampsShape, firestoreDateSchema, paiseSchema } from "../../../schemas/firestore-helpers";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SavedPaymentMethod Firestore Document Types & Constants
|
|
3
|
+
*
|
|
4
|
+
* Stores user-saved payment identifiers (UPI VPAs, card tokens, bank accounts)
|
|
5
|
+
* for checkout pre-fill and cross-account fraud detection.
|
|
6
|
+
*
|
|
7
|
+
* PII: `identifier` (full UPI VPA / card PAN) is encrypted at rest via
|
|
8
|
+
* encryptPiiFields. `identifierHash` is a deterministic SHA-256 of the
|
|
9
|
+
* normalised identifier and stored unencrypted for cross-account queries.
|
|
10
|
+
* `displayLabel` is a pre-masked string and never contains full PII.
|
|
11
|
+
*/
|
|
12
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
13
|
+
export type SavedPaymentMethodType = "upi" | "card" | "bank_account" | "wallet";
|
|
14
|
+
export type SavedPaymentMethodBanStatus = "banned" | "suspicious" | "unban_requested";
|
|
15
|
+
export interface SavedPaymentMethodDocument extends BaseDocument {
|
|
16
|
+
userId: string;
|
|
17
|
+
type: SavedPaymentMethodType;
|
|
18
|
+
/** Full UPI VPA / card PAN / account number — ENCRYPTED at rest. */
|
|
19
|
+
identifier: string;
|
|
20
|
+
/** Pre-masked display string, never contains full PII. e.g. "9876...@paytm", "XXXX 4321 (HDFC)" */
|
|
21
|
+
displayLabel: string;
|
|
22
|
+
/** SHA-256(type|normalised(identifier)) — unencrypted; enables cross-account dedup without decrypting. */
|
|
23
|
+
identifierHash: string;
|
|
24
|
+
isDefault?: boolean;
|
|
25
|
+
lastUsedAt?: Date;
|
|
26
|
+
banStatus?: SavedPaymentMethodBanStatus;
|
|
27
|
+
banReason?: string;
|
|
28
|
+
bannedBy?: string;
|
|
29
|
+
bannedAt?: Date;
|
|
30
|
+
autoBanned?: boolean;
|
|
31
|
+
unbanRequestNote?: string;
|
|
32
|
+
unbanRequestedAt?: Date;
|
|
33
|
+
}
|
|
34
|
+
export declare const SAVED_PAYMENT_METHODS_COLLECTION: "savedPaymentMethods";
|
|
35
|
+
export declare const SAVED_PAYMENT_METHOD_INDEXED_FIELDS: readonly ["userId", "identifierHash", "banStatus", "type", "createdAt"];
|
|
36
|
+
export declare const SAVED_PAYMENT_METHOD_PUBLIC_FIELDS: readonly ["id", "userId", "type", "displayLabel", "identifierHash", "isDefault", "lastUsedAt", "banStatus", "banReason", "bannedAt", "unbanRequestNote", "unbanRequestedAt", "createdAt", "updatedAt"];
|
|
37
|
+
export declare const SAVED_PAYMENT_METHOD_UPDATABLE_FIELDS: readonly ["displayLabel", "isDefault", "lastUsedAt", "unbanRequestNote", "unbanRequestedAt"];
|
|
38
|
+
export declare const SAVED_PAYMENT_METHOD_FIELDS: {
|
|
39
|
+
readonly USER_ID: "userId";
|
|
40
|
+
readonly TYPE: "type";
|
|
41
|
+
readonly IDENTIFIER: "identifier";
|
|
42
|
+
readonly DISPLAY_LABEL: "displayLabel";
|
|
43
|
+
readonly IDENTIFIER_HASH: "identifierHash";
|
|
44
|
+
readonly IS_DEFAULT: "isDefault";
|
|
45
|
+
readonly LAST_USED_AT: "lastUsedAt";
|
|
46
|
+
readonly BAN_STATUS: "banStatus";
|
|
47
|
+
readonly BAN_REASON: "banReason";
|
|
48
|
+
readonly BANNED_BY: "bannedBy";
|
|
49
|
+
readonly BANNED_AT: "bannedAt";
|
|
50
|
+
readonly AUTO_BANNED: "autoBanned";
|
|
51
|
+
readonly UNBAN_REQUEST_NOTE: "unbanRequestNote";
|
|
52
|
+
readonly UNBAN_REQUESTED_AT: "unbanRequestedAt";
|
|
53
|
+
readonly CREATED_AT: "createdAt";
|
|
54
|
+
readonly UPDATED_AT: "updatedAt";
|
|
55
|
+
};
|
|
56
|
+
export type SavedPaymentMethodCreateInput = {
|
|
57
|
+
type: SavedPaymentMethodType;
|
|
58
|
+
/** Raw identifier — will be encrypted + hashed by repository. */
|
|
59
|
+
identifier: string;
|
|
60
|
+
displayLabel: string;
|
|
61
|
+
isDefault?: boolean;
|
|
62
|
+
};
|
|
63
|
+
export type SavedPaymentMethodUpdateInput = Partial<Pick<SavedPaymentMethodDocument, (typeof SAVED_PAYMENT_METHOD_UPDATABLE_FIELDS)[number]>>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SavedPaymentMethod Firestore Document Types & Constants
|
|
3
|
+
*
|
|
4
|
+
* Stores user-saved payment identifiers (UPI VPAs, card tokens, bank accounts)
|
|
5
|
+
* for checkout pre-fill and cross-account fraud detection.
|
|
6
|
+
*
|
|
7
|
+
* PII: `identifier` (full UPI VPA / card PAN) is encrypted at rest via
|
|
8
|
+
* encryptPiiFields. `identifierHash` is a deterministic SHA-256 of the
|
|
9
|
+
* normalised identifier and stored unencrypted for cross-account queries.
|
|
10
|
+
* `displayLabel` is a pre-masked string and never contains full PII.
|
|
11
|
+
*/
|
|
12
|
+
export const SAVED_PAYMENT_METHODS_COLLECTION = "savedPaymentMethods";
|
|
13
|
+
export const SAVED_PAYMENT_METHOD_INDEXED_FIELDS = [
|
|
14
|
+
"userId",
|
|
15
|
+
"identifierHash",
|
|
16
|
+
"banStatus",
|
|
17
|
+
"type",
|
|
18
|
+
"createdAt",
|
|
19
|
+
];
|
|
20
|
+
export const SAVED_PAYMENT_METHOD_PUBLIC_FIELDS = [
|
|
21
|
+
"id",
|
|
22
|
+
"userId",
|
|
23
|
+
"type",
|
|
24
|
+
"displayLabel",
|
|
25
|
+
"identifierHash",
|
|
26
|
+
"isDefault",
|
|
27
|
+
"lastUsedAt",
|
|
28
|
+
"banStatus",
|
|
29
|
+
"banReason",
|
|
30
|
+
"bannedAt",
|
|
31
|
+
"unbanRequestNote",
|
|
32
|
+
"unbanRequestedAt",
|
|
33
|
+
"createdAt",
|
|
34
|
+
"updatedAt",
|
|
35
|
+
];
|
|
36
|
+
export const SAVED_PAYMENT_METHOD_UPDATABLE_FIELDS = [
|
|
37
|
+
"displayLabel",
|
|
38
|
+
"isDefault",
|
|
39
|
+
"lastUsedAt",
|
|
40
|
+
"unbanRequestNote",
|
|
41
|
+
"unbanRequestedAt",
|
|
42
|
+
];
|
|
43
|
+
export const SAVED_PAYMENT_METHOD_FIELDS = {
|
|
44
|
+
USER_ID: "userId",
|
|
45
|
+
TYPE: "type",
|
|
46
|
+
IDENTIFIER: "identifier",
|
|
47
|
+
DISPLAY_LABEL: "displayLabel",
|
|
48
|
+
IDENTIFIER_HASH: "identifierHash",
|
|
49
|
+
IS_DEFAULT: "isDefault",
|
|
50
|
+
LAST_USED_AT: "lastUsedAt",
|
|
51
|
+
BAN_STATUS: "banStatus",
|
|
52
|
+
BAN_REASON: "banReason",
|
|
53
|
+
BANNED_BY: "bannedBy",
|
|
54
|
+
BANNED_AT: "bannedAt",
|
|
55
|
+
AUTO_BANNED: "autoBanned",
|
|
56
|
+
UNBAN_REQUEST_NOTE: "unbanRequestNote",
|
|
57
|
+
UNBAN_REQUESTED_AT: "unbanRequestedAt",
|
|
58
|
+
CREATED_AT: "createdAt",
|
|
59
|
+
UPDATED_AT: "updatedAt",
|
|
60
|
+
};
|
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { PaymentsRepository } from "./repository/payments.repository";
|
|
7
7
|
export { payoutRepository } from "./repository/payout.repository";
|
|
8
|
+
export { savedPaymentMethodsRepository, SavedPaymentMethodsRepository, } from "./repository/saved-payment-methods.repository";
|
|
9
|
+
export * from "./schemas/saved-methods-firestore";
|
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { PaymentsRepository } from "./repository/payments.repository";
|
|
7
7
|
export { payoutRepository } from "./repository/payout.repository";
|
|
8
|
+
export { savedPaymentMethodsRepository, SavedPaymentMethodsRepository, } from "./repository/saved-payment-methods.repository";
|
|
9
|
+
export * from "./schemas/saved-methods-firestore";
|
|
@@ -405,22 +405,22 @@ export declare const couponUsageFirestoreSchema: z.ZodObject<{
|
|
|
405
405
|
orders: z.ZodArray<z.ZodString, "many">;
|
|
406
406
|
}, "strip", z.ZodTypeAny, {
|
|
407
407
|
id: string;
|
|
408
|
-
orders: string[];
|
|
409
408
|
userId: string;
|
|
410
|
-
couponCode: string;
|
|
411
|
-
usageCount: number;
|
|
412
409
|
lastUsedAt: string | Date | z.objectOutputType<{
|
|
413
410
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
414
411
|
}, z.ZodTypeAny, "passthrough">;
|
|
415
|
-
}, {
|
|
416
|
-
id: string;
|
|
417
412
|
orders: string[];
|
|
418
|
-
userId: string;
|
|
419
413
|
couponCode: string;
|
|
420
414
|
usageCount: number;
|
|
415
|
+
}, {
|
|
416
|
+
id: string;
|
|
417
|
+
userId: string;
|
|
421
418
|
lastUsedAt: string | Date | z.objectInputType<{
|
|
422
419
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
423
420
|
}, z.ZodTypeAny, "passthrough">;
|
|
421
|
+
orders: string[];
|
|
422
|
+
couponCode: string;
|
|
423
|
+
usageCount: number;
|
|
424
424
|
}>;
|
|
425
425
|
export declare const claimedCouponStatusSchema: z.ZodEnum<["active", "expired", "used"]>;
|
|
426
426
|
export declare const claimedCouponSourceSchema: z.ZodEnum<["manual", "promo", "spin", "raffle", "prize-draw"]>;
|
|
@@ -35,6 +35,10 @@ declare class ReviewRepository extends BaseRepository<ReviewDocument> {
|
|
|
35
35
|
* Cloud Functions compatibility: approved review count + average by store.
|
|
36
36
|
*/
|
|
37
37
|
getApprovedRatingAggregateByStore(storeId: string): Promise<ReviewRatingAggregate>;
|
|
38
|
+
/** Find reviews where this user is the reviewee (seller→buyer reviews received by a buyer). */
|
|
39
|
+
findByReviewee(revieweeId: string): Promise<ReviewDocument[]>;
|
|
40
|
+
/** Find reviews written by a user filtered by their role as reviewer. */
|
|
41
|
+
findByUserAsRole(userId: string, role: "buyer" | "seller"): Promise<ReviewDocument[]>;
|
|
38
42
|
static readonly SIEVE_FIELDS: {
|
|
39
43
|
id: {
|
|
40
44
|
canFilter: boolean;
|
|
@@ -68,6 +72,14 @@ declare class ReviewRepository extends BaseRepository<ReviewDocument> {
|
|
|
68
72
|
canFilter: boolean;
|
|
69
73
|
canSort: boolean;
|
|
70
74
|
};
|
|
75
|
+
revieweeId: {
|
|
76
|
+
canFilter: boolean;
|
|
77
|
+
canSort: boolean;
|
|
78
|
+
};
|
|
79
|
+
reviewerRole: {
|
|
80
|
+
canFilter: boolean;
|
|
81
|
+
canSort: boolean;
|
|
82
|
+
};
|
|
71
83
|
status: {
|
|
72
84
|
canFilter: boolean;
|
|
73
85
|
canSort: boolean;
|
|
@@ -8,6 +8,8 @@ const REVIEW_FIELDS = {
|
|
|
8
8
|
STATUS: "status",
|
|
9
9
|
FEATURED: "featured",
|
|
10
10
|
CREATED_AT: "createdAt",
|
|
11
|
+
REVIEWEE_ID: "revieweeId",
|
|
12
|
+
REVIEWER_ROLE: "reviewerRole",
|
|
11
13
|
};
|
|
12
14
|
class ReviewRepository extends BaseRepository {
|
|
13
15
|
constructor() {
|
|
@@ -197,6 +199,26 @@ class ReviewRepository extends BaseRepository {
|
|
|
197
199
|
const avgRating = Math.round((sum / count) * 10) / 10;
|
|
198
200
|
return { count, avgRating };
|
|
199
201
|
}
|
|
202
|
+
/** Find reviews where this user is the reviewee (seller→buyer reviews received by a buyer). */
|
|
203
|
+
async findByReviewee(revieweeId) {
|
|
204
|
+
const snapshot = await this.db
|
|
205
|
+
.collection(this.collection)
|
|
206
|
+
.where(REVIEW_FIELDS.REVIEWEE_ID, "==", revieweeId)
|
|
207
|
+
.where(REVIEW_FIELDS.STATUS, "==", "approved")
|
|
208
|
+
.orderBy(REVIEW_FIELDS.CREATED_AT, "desc")
|
|
209
|
+
.get();
|
|
210
|
+
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
211
|
+
}
|
|
212
|
+
/** Find reviews written by a user filtered by their role as reviewer. */
|
|
213
|
+
async findByUserAsRole(userId, role) {
|
|
214
|
+
const snapshot = await this.db
|
|
215
|
+
.collection(this.collection)
|
|
216
|
+
.where(REVIEW_FIELDS.USER_ID, "==", userId)
|
|
217
|
+
.where(REVIEW_FIELDS.REVIEWER_ROLE, "==", role)
|
|
218
|
+
.orderBy(REVIEW_FIELDS.CREATED_AT, "desc")
|
|
219
|
+
.get();
|
|
220
|
+
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
221
|
+
}
|
|
200
222
|
async listForProduct(productId, model) {
|
|
201
223
|
const baseQuery = this.getCollection().where(REVIEW_FIELDS.PRODUCT_ID, "==", productId);
|
|
202
224
|
const result = await this.sieveQuery(model, ReviewRepository.SIEVE_FIELDS, {
|
|
@@ -232,6 +254,8 @@ ReviewRepository.SIEVE_FIELDS = {
|
|
|
232
254
|
userName: { canFilter: false, canSort: false },
|
|
233
255
|
storeId: { canFilter: true, canSort: false },
|
|
234
256
|
storeName: { canFilter: true, canSort: false },
|
|
257
|
+
revieweeId: { canFilter: true, canSort: false },
|
|
258
|
+
reviewerRole: { canFilter: true, canSort: false },
|
|
235
259
|
status: { canFilter: true, canSort: true },
|
|
236
260
|
rating: { canFilter: true, canSort: true },
|
|
237
261
|
verified: { canFilter: true, canSort: false },
|
|
@@ -46,9 +46,13 @@ export interface ReviewDocument extends BaseDocument {
|
|
|
46
46
|
sellerRepliedAt?: Date;
|
|
47
47
|
approvedAt?: Date;
|
|
48
48
|
rejectedAt?: Date;
|
|
49
|
+
/** Set only on seller→buyer reviews. The userId of the buyer being reviewed. */
|
|
50
|
+
revieweeId?: string;
|
|
51
|
+
/** Absent on legacy docs = "buyer". Distinguishes seller→buyer reviews from buyer→product reviews. */
|
|
52
|
+
reviewerRole?: "buyer" | "seller";
|
|
49
53
|
}
|
|
50
54
|
export declare const REVIEW_COLLECTION: "reviews";
|
|
51
|
-
export declare const REVIEW_INDEXED_FIELDS: readonly ["productId", "storeId", "userId", "status", "rating", "verified", "featured", "createdAt"];
|
|
55
|
+
export declare const REVIEW_INDEXED_FIELDS: readonly ["productId", "storeId", "userId", "revieweeId", "reviewerRole", "status", "rating", "verified", "featured", "createdAt"];
|
|
52
56
|
export declare const DEFAULT_REVIEW_DATA: Partial<ReviewDocument>;
|
|
53
57
|
export declare const REVIEW_PUBLIC_FIELDS: readonly ["id", "productId", "userName", "userAvatar", "rating", "title", "comment", "helpfulCount", "verified", "images", "createdAt"];
|
|
54
58
|
export declare const REVIEW_UPDATABLE_FIELDS: readonly ["rating", "title", "comment", "images"];
|
|
@@ -108,10 +108,10 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
108
108
|
updatedAt: string | Date | z.objectOutputType<{
|
|
109
109
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
110
110
|
}, z.ZodTypeAny, "passthrough">;
|
|
111
|
+
userId: string;
|
|
111
112
|
productId: string;
|
|
112
113
|
productTitle: string;
|
|
113
114
|
rating: number;
|
|
114
|
-
userId: string;
|
|
115
115
|
userName: string;
|
|
116
116
|
comment: string;
|
|
117
117
|
helpfulCount: number;
|
|
@@ -155,10 +155,10 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
155
155
|
updatedAt: string | Date | z.objectInputType<{
|
|
156
156
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
157
157
|
}, z.ZodTypeAny, "passthrough">;
|
|
158
|
+
userId: string;
|
|
158
159
|
productId: string;
|
|
159
160
|
productTitle: string;
|
|
160
161
|
rating: number;
|
|
161
|
-
userId: string;
|
|
162
162
|
userName: string;
|
|
163
163
|
comment: string;
|
|
164
164
|
helpfulCount: number;
|
|
@@ -288,9 +288,9 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
288
288
|
}, "strip", z.ZodTypeAny, {
|
|
289
289
|
status: "pending" | "approved" | "rejected";
|
|
290
290
|
id: string;
|
|
291
|
+
userId: string;
|
|
291
292
|
productId: string;
|
|
292
293
|
rating: 3 | 1 | 2 | 4 | 5;
|
|
293
|
-
userId: string;
|
|
294
294
|
userName: string;
|
|
295
295
|
video?: {
|
|
296
296
|
url: string;
|
|
@@ -318,9 +318,9 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
318
318
|
}, {
|
|
319
319
|
status: "pending" | "approved" | "rejected";
|
|
320
320
|
id: string;
|
|
321
|
+
userId: string;
|
|
321
322
|
productId: string;
|
|
322
323
|
rating: 3 | 1 | 2 | 4 | 5;
|
|
323
|
-
userId: string;
|
|
324
324
|
userName: string;
|
|
325
325
|
video?: {
|
|
326
326
|
url: string;
|
|
@@ -364,9 +364,9 @@ export declare const reviewListParamsSchema: z.ZodObject<{
|
|
|
364
364
|
featured?: boolean | undefined;
|
|
365
365
|
page?: number | undefined;
|
|
366
366
|
perPage?: number | undefined;
|
|
367
|
+
userId?: string | undefined;
|
|
367
368
|
productId?: string | undefined;
|
|
368
369
|
rating?: number | undefined;
|
|
369
|
-
userId?: string | undefined;
|
|
370
370
|
}, {
|
|
371
371
|
status?: "pending" | "approved" | "rejected" | undefined;
|
|
372
372
|
sort?: string | undefined;
|
|
@@ -374,7 +374,7 @@ export declare const reviewListParamsSchema: z.ZodObject<{
|
|
|
374
374
|
featured?: boolean | undefined;
|
|
375
375
|
page?: number | undefined;
|
|
376
376
|
perPage?: number | undefined;
|
|
377
|
+
userId?: string | undefined;
|
|
377
378
|
productId?: string | undefined;
|
|
378
379
|
rating?: number | undefined;
|
|
379
|
-
userId?: string | undefined;
|
|
380
380
|
}>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { MapPin, Pencil, Plus, Trash2, Star } from "lucide-react";
|
|
5
|
-
import { Button, ConfirmDeleteModal, Div, Grid, Heading, Label, Row, SideDrawer, Span, Stack, Table, Thead, Tbody, Tr, Th, Td, Text } from "../../../ui";
|
|
5
|
+
import { Badge, Button, ConfirmDeleteModal, Div, Grid, Heading, Label, Row, SideDrawer, Span, Stack, Table, Thead, Tbody, Tr, Th, Td, Text } from "../../../ui";
|
|
6
6
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
7
7
|
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
8
8
|
import { ROW_ACTION_META, ROW_ACTION_ID } from "../../../features/products/constants/action-defs";
|
|
@@ -50,7 +50,8 @@ function fromDoc(doc) {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
function AddressCard({ address, onEdit, onDelete, }) {
|
|
53
|
-
|
|
53
|
+
const isBanned = address.banStatus === "banned";
|
|
54
|
+
return (_jsxs(Stack, { surface: "card", padding: "sm", gap: "sm", children: [_jsxs(Row, { align: "start", justify: "between", gap: "xs", children: [_jsxs(Row, { gap: "xs", wrap: true, className: "min-w-0", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-[var(--appkit-color-primary)]" }), _jsx(Span, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: address.label }), address.isDefault && (_jsxs(Span, { size: "xs", weight: "medium", className: CLS_DEFAULT_PILL, children: [_jsx(Star, { className: "h-3 w-3" }), "Default"] })), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }), _jsx(Row, { gap: "px", className: "shrink-0", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: onEdit, title: "Edit address", className: "rounded-lg p-1.5 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors", children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", onClick: onDelete, title: "Delete address", className: CLS_DELETE_BTN, children: _jsx(Trash2, { className: "h-4 w-4" }) })] })) })] }), _jsxs(Text, { size: "sm", color: "muted", children: [address.fullName, " \u00B7 ", address.phone] }), _jsxs(Text, { className: "leading-relaxed", color: "muted", size: "xs", children: [address.addressLine1, address.addressLine2 ? `, ${address.addressLine2}` : "", address.landmark ? ` (near ${address.landmark})` : "", _jsx("br", {}), address.city, ", ", address.state, " ", address.postalCode, ", ", address.country] })] }));
|
|
54
55
|
}
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
56
57
|
// Main Component
|
|
@@ -143,5 +144,8 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
|
|
|
143
144
|
const handleDelete = (addr) => setDeleteTargetAddr(addr);
|
|
144
145
|
const set = (key, value) => setDraft((p) => ({ ...p, [key]: value }));
|
|
145
146
|
const setField = (key) => (value) => set(key, value);
|
|
146
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsxs(Row, { border: "default", justify: "between", className: "sticky top-[var(--header-height,0px)] z-10 backdrop-blur-sm border-b", surface: "default", padding: "inline", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-zinc-300 dark:text-slate-600" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) =>
|
|
147
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsxs(Row, { border: "default", justify: "between", className: "sticky top-[var(--header-height,0px)] z-10 backdrop-blur-sm border-b", surface: "default", padding: "inline", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-zinc-300 dark:text-slate-600" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
|
|
148
|
+
const isBanned = addr.banStatus === "banned";
|
|
149
|
+
return (_jsxs(Tr, { className: `border-t border-[var(--appkit-color-border)] ${deletingId === addr.id ? "opacity-50" : ""}`, children: [_jsx(Td, { padding: "sm", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { children: addr.label }), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }) }), _jsx(Td, { padding: "sm", children: addr.fullName }), _jsxs(Td, { padding: "sm", children: [addr.city, ", ", addr.state] }), _jsx(Td, { className: "tabular-nums", padding: "sm", children: addr.phone }), _jsx(Td, { className: "text-right", padding: "sm", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(Row, { justify: "end", gap: "xs", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => openEdit(addr), children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => handleDelete(addr), children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label })] })) })] }, addr.id));
|
|
150
|
+
}) })] }) }))] }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer: _jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "outline", onClick: closeDrawer, className: "flex-1", children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: saving, className: "flex-1", children: saving ? "Saving…" : editingId ? "Save Changes" : "Add Address" })] }), children: _jsxs(Stack, { gap: "md", padding: "y-2xs", children: [saveError && (_jsx(Div, { textSize: "sm", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError })), _jsx(FieldInput, { name: "label", label: "Label *", hint: "e.g. Warehouse, Shop, Home", type: "text", value: draft.label, onChange: setField("label"), placeholder: "Warehouse", maxLength: 60 }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "fullName", label: "Full Name *", type: "text", value: draft.fullName, onChange: setField("fullName"), placeholder: "Ravi Kumar", maxLength: 100 }), _jsx(FieldInput, { name: "phone", label: "Phone *", type: "tel", value: draft.phone, onChange: setField("phone"), placeholder: "+91 98765 43210", maxLength: 20 })] }), _jsx(FieldInput, { name: "addressLine1", label: "Address Line 1 *", type: "text", value: draft.addressLine1, onChange: setField("addressLine1"), placeholder: "Shop 12, Main Market", maxLength: 200 }), _jsx(FieldInput, { name: "addressLine2", label: "Address Line 2", type: "text", value: draft.addressLine2, onChange: setField("addressLine2"), placeholder: "Building / Floor (optional)", maxLength: 200 }), _jsx(FieldInput, { name: "landmark", label: "Landmark", type: "text", value: draft.landmark, onChange: setField("landmark"), placeholder: "Near metro station (optional)", maxLength: 100 }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "city", label: "City *", type: "text", value: draft.city, onChange: setField("city"), placeholder: "Mumbai", maxLength: 100 }), _jsx(FieldInput, { name: "state", label: "State *", type: "text", value: draft.state, onChange: setField("state"), placeholder: "Maharashtra", maxLength: 100 })] }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "postalCode", label: "Postal Code *", type: "text", value: draft.postalCode, onChange: setField("postalCode"), placeholder: "400001", maxLength: 10 }), _jsx(FieldInput, { name: "country", label: "Country *", type: "text", value: draft.country, onChange: setField("country"), placeholder: "India", maxLength: 60 })] }), _jsx(FieldCheckbox, { name: "isDefault", label: "Set as default pickup address", checked: draft.isDefault, onChange: (c) => set("isDefault", c) })] }) }), deleteTargetAddr && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Address", message: `Delete address "${deleteTargetAddr.label}"? This cannot be undone.`, onConfirm: () => { deleteById(deleteTargetAddr.id); setDeleteTargetAddr(null); }, onClose: () => setDeleteTargetAddr(null), isDeleting: deletingId === deleteTargetAddr.id }))] }));
|
|
147
151
|
}
|