@rajeev02/app-shell 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analytics/index.d.ts +42 -0
- package/lib/analytics/index.d.ts.map +1 -0
- package/lib/analytics/index.js +69 -0
- package/lib/analytics/index.js.map +1 -0
- package/lib/api/index.d.ts +84 -0
- package/lib/api/index.d.ts.map +1 -0
- package/lib/api/index.js +219 -0
- package/lib/api/index.js.map +1 -0
- package/lib/cart/index.d.ts +97 -0
- package/lib/cart/index.d.ts.map +1 -0
- package/lib/cart/index.js +134 -0
- package/lib/cart/index.js.map +1 -0
- package/lib/chat/index.d.ts +111 -0
- package/lib/chat/index.d.ts.map +1 -0
- package/lib/chat/index.js +169 -0
- package/lib/chat/index.js.map +1 -0
- package/lib/config/index.d.ts +33 -0
- package/lib/config/index.d.ts.map +1 -0
- package/lib/config/index.js +62 -0
- package/lib/config/index.js.map +1 -0
- package/lib/forms/index.d.ts +78 -0
- package/lib/forms/index.d.ts.map +1 -0
- package/lib/forms/index.js +292 -0
- package/lib/forms/index.js.map +1 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +35 -0
- package/lib/index.js.map +1 -0
- package/lib/onboarding/index.d.ts +62 -0
- package/lib/onboarding/index.d.ts.map +1 -0
- package/lib/onboarding/index.js +117 -0
- package/lib/onboarding/index.js.map +1 -0
- package/package.json +51 -0
- package/src/analytics/index.ts +92 -0
- package/src/api/index.ts +322 -0
- package/src/cart/index.ts +213 -0
- package/src/chat/index.ts +260 -0
- package/src/config/index.ts +69 -0
- package/src/forms/index.ts +376 -0
- package/src/index.ts +68 -0
- package/src/onboarding/index.ts +159 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rajeev02/app-shell — Chat Module
|
|
3
|
+
* Real-time messaging with offline queue, typing indicators, media messages, read receipts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type MessageType =
|
|
7
|
+
| "text"
|
|
8
|
+
| "image"
|
|
9
|
+
| "video"
|
|
10
|
+
| "audio"
|
|
11
|
+
| "document"
|
|
12
|
+
| "location"
|
|
13
|
+
| "contact"
|
|
14
|
+
| "sticker";
|
|
15
|
+
export type MessageStatus =
|
|
16
|
+
| "sending"
|
|
17
|
+
| "sent"
|
|
18
|
+
| "delivered"
|
|
19
|
+
| "read"
|
|
20
|
+
| "failed";
|
|
21
|
+
|
|
22
|
+
export interface ChatMessage {
|
|
23
|
+
id: string;
|
|
24
|
+
roomId: string;
|
|
25
|
+
senderId: string;
|
|
26
|
+
type: MessageType;
|
|
27
|
+
content: string;
|
|
28
|
+
mediaUrl?: string;
|
|
29
|
+
thumbnailUrl?: string;
|
|
30
|
+
mediaSizeBytes?: number;
|
|
31
|
+
replyTo?: string;
|
|
32
|
+
forwardedFrom?: string;
|
|
33
|
+
status: MessageStatus;
|
|
34
|
+
timestamp: number;
|
|
35
|
+
editedAt?: number;
|
|
36
|
+
deletedAt?: number;
|
|
37
|
+
reactions?: Record<string, string[]>;
|
|
38
|
+
metadata?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ChatRoom {
|
|
42
|
+
id: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
type: "direct" | "group" | "channel" | "support";
|
|
45
|
+
participants: string[];
|
|
46
|
+
lastMessage?: ChatMessage;
|
|
47
|
+
unreadCount: number;
|
|
48
|
+
muted: boolean;
|
|
49
|
+
pinned: boolean;
|
|
50
|
+
createdAt: number;
|
|
51
|
+
updatedAt: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TypingIndicator {
|
|
55
|
+
roomId: string;
|
|
56
|
+
userId: string;
|
|
57
|
+
isTyping: boolean;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Chat Engine — manages rooms, messages, offline queue
|
|
63
|
+
*/
|
|
64
|
+
export class ChatEngine {
|
|
65
|
+
private rooms: Map<string, ChatRoom> = new Map();
|
|
66
|
+
private messages: Map<string, ChatMessage[]> = new Map();
|
|
67
|
+
private offlineQueue: ChatMessage[] = [];
|
|
68
|
+
private typingUsers: Map<string, Set<string>> = new Map();
|
|
69
|
+
private listeners: Map<string, Set<(event: ChatEvent) => void>> = new Map();
|
|
70
|
+
private currentUserId: string;
|
|
71
|
+
|
|
72
|
+
constructor(userId: string) {
|
|
73
|
+
this.currentUserId = userId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Send a text message */
|
|
77
|
+
sendMessage(
|
|
78
|
+
roomId: string,
|
|
79
|
+
content: string,
|
|
80
|
+
type: MessageType = "text",
|
|
81
|
+
replyTo?: string,
|
|
82
|
+
): ChatMessage {
|
|
83
|
+
const message: ChatMessage = {
|
|
84
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
85
|
+
roomId,
|
|
86
|
+
senderId: this.currentUserId,
|
|
87
|
+
type,
|
|
88
|
+
content,
|
|
89
|
+
status: "sending",
|
|
90
|
+
timestamp: Date.now(),
|
|
91
|
+
replyTo,
|
|
92
|
+
};
|
|
93
|
+
this.addMessage(roomId, message);
|
|
94
|
+
this.offlineQueue.push(message);
|
|
95
|
+
this.emit(roomId, { type: "message_sent", message });
|
|
96
|
+
return message;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Receive a message (from server/websocket) */
|
|
100
|
+
receiveMessage(message: ChatMessage): void {
|
|
101
|
+
this.addMessage(message.roomId, message);
|
|
102
|
+
this.updateRoom(message.roomId, { lastMessage: message });
|
|
103
|
+
this.emit(message.roomId, { type: "message_received", message });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Mark a message as delivered/read */
|
|
107
|
+
updateMessageStatus(
|
|
108
|
+
messageId: string,
|
|
109
|
+
roomId: string,
|
|
110
|
+
status: MessageStatus,
|
|
111
|
+
): void {
|
|
112
|
+
const messages = this.messages.get(roomId);
|
|
113
|
+
const msg = messages?.find((m) => m.id === messageId);
|
|
114
|
+
if (msg) {
|
|
115
|
+
msg.status = status;
|
|
116
|
+
this.emit(roomId, { type: "status_update", messageId, status });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Mark all messages in room as read */
|
|
121
|
+
markRoomAsRead(roomId: string): void {
|
|
122
|
+
const room = this.rooms.get(roomId);
|
|
123
|
+
if (room) {
|
|
124
|
+
room.unreadCount = 0;
|
|
125
|
+
}
|
|
126
|
+
this.emit(roomId, { type: "room_read", roomId });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Delete a message (soft delete) */
|
|
130
|
+
deleteMessage(messageId: string, roomId: string): void {
|
|
131
|
+
const messages = this.messages.get(roomId);
|
|
132
|
+
const msg = messages?.find((m) => m.id === messageId);
|
|
133
|
+
if (msg) {
|
|
134
|
+
msg.deletedAt = Date.now();
|
|
135
|
+
msg.content = "";
|
|
136
|
+
}
|
|
137
|
+
this.emit(roomId, { type: "message_deleted", messageId });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Add reaction */
|
|
141
|
+
addReaction(messageId: string, roomId: string, emoji: string): void {
|
|
142
|
+
const messages = this.messages.get(roomId);
|
|
143
|
+
const msg = messages?.find((m) => m.id === messageId);
|
|
144
|
+
if (msg) {
|
|
145
|
+
if (!msg.reactions) msg.reactions = {};
|
|
146
|
+
if (!msg.reactions[emoji]) msg.reactions[emoji] = [];
|
|
147
|
+
if (!msg.reactions[emoji].includes(this.currentUserId)) {
|
|
148
|
+
msg.reactions[emoji].push(this.currentUserId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Set typing indicator */
|
|
154
|
+
setTyping(roomId: string, isTyping: boolean): void {
|
|
155
|
+
this.emit(roomId, { type: "typing", userId: this.currentUserId, isTyping });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Handle remote typing indicator */
|
|
159
|
+
onRemoteTyping(roomId: string, userId: string, isTyping: boolean): void {
|
|
160
|
+
if (!this.typingUsers.has(roomId)) this.typingUsers.set(roomId, new Set());
|
|
161
|
+
const users = this.typingUsers.get(roomId)!;
|
|
162
|
+
if (isTyping) users.add(userId);
|
|
163
|
+
else users.delete(userId);
|
|
164
|
+
this.emit(roomId, { type: "typing", userId, isTyping });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Get typing users in a room */
|
|
168
|
+
getTypingUsers(roomId: string): string[] {
|
|
169
|
+
return Array.from(this.typingUsers.get(roomId) ?? []);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Get messages for a room */
|
|
173
|
+
getMessages(
|
|
174
|
+
roomId: string,
|
|
175
|
+
limit: number = 50,
|
|
176
|
+
before?: number,
|
|
177
|
+
): ChatMessage[] {
|
|
178
|
+
const all = this.messages.get(roomId) ?? [];
|
|
179
|
+
let filtered = before ? all.filter((m) => m.timestamp < before) : all;
|
|
180
|
+
return filtered.slice(-limit);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Get all rooms sorted by last activity */
|
|
184
|
+
getRooms(): ChatRoom[] {
|
|
185
|
+
return Array.from(this.rooms.values()).sort(
|
|
186
|
+
(a, b) => b.updatedAt - a.updatedAt,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Get total unread count */
|
|
191
|
+
getTotalUnread(): number {
|
|
192
|
+
return Array.from(this.rooms.values()).reduce(
|
|
193
|
+
(sum, r) => sum + r.unreadCount,
|
|
194
|
+
0,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Get offline queue for syncing */
|
|
199
|
+
getOfflineQueue(): ChatMessage[] {
|
|
200
|
+
return [...this.offlineQueue];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Clear offline queue after successful sync */
|
|
204
|
+
clearOfflineQueue(): void {
|
|
205
|
+
this.offlineQueue = [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Search messages across rooms */
|
|
209
|
+
search(query: string, limit: number = 20): ChatMessage[] {
|
|
210
|
+
const results: ChatMessage[] = [];
|
|
211
|
+
const lower = query.toLowerCase();
|
|
212
|
+
for (const messages of this.messages.values()) {
|
|
213
|
+
for (const msg of messages) {
|
|
214
|
+
if (msg.content.toLowerCase().includes(lower) && !msg.deletedAt) {
|
|
215
|
+
results.push(msg);
|
|
216
|
+
if (results.length >= limit) return results;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Subscribe to room events */
|
|
224
|
+
onRoomEvent(
|
|
225
|
+
roomId: string,
|
|
226
|
+
listener: (event: ChatEvent) => void,
|
|
227
|
+
): () => void {
|
|
228
|
+
if (!this.listeners.has(roomId)) this.listeners.set(roomId, new Set());
|
|
229
|
+
this.listeners.get(roomId)!.add(listener);
|
|
230
|
+
return () => this.listeners.get(roomId)?.delete(listener);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private addMessage(roomId: string, message: ChatMessage): void {
|
|
234
|
+
if (!this.messages.has(roomId)) this.messages.set(roomId, []);
|
|
235
|
+
this.messages.get(roomId)!.push(message);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private updateRoom(roomId: string, updates: Partial<ChatRoom>): void {
|
|
239
|
+
const room = this.rooms.get(roomId);
|
|
240
|
+
if (room) Object.assign(room, updates, { updatedAt: Date.now() });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private emit(roomId: string, event: ChatEvent): void {
|
|
244
|
+
const listeners = this.listeners.get(roomId);
|
|
245
|
+
if (listeners)
|
|
246
|
+
for (const l of listeners) {
|
|
247
|
+
try {
|
|
248
|
+
l(event);
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type ChatEvent =
|
|
255
|
+
| { type: "message_sent"; message: ChatMessage }
|
|
256
|
+
| { type: "message_received"; message: ChatMessage }
|
|
257
|
+
| { type: "message_deleted"; messageId: string }
|
|
258
|
+
| { type: "status_update"; messageId: string; status: MessageStatus }
|
|
259
|
+
| { type: "typing"; userId: string; isTyping: boolean }
|
|
260
|
+
| { type: "room_read"; roomId: string };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rajeev02/app-shell — Feature Flags & Remote Config
|
|
3
|
+
* A/B testing, gradual rollout, kill switch, remote configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FeatureFlag {
|
|
7
|
+
key: string;
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
rolloutPercent?: number;
|
|
10
|
+
segments?: string[];
|
|
11
|
+
variant?: string;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class FeatureFlagManager {
|
|
16
|
+
private flags: Map<string, FeatureFlag> = new Map();
|
|
17
|
+
private config: Map<string, unknown> = new Map();
|
|
18
|
+
private userId: string = "";
|
|
19
|
+
|
|
20
|
+
/** Load flags from server response */
|
|
21
|
+
loadFlags(flags: FeatureFlag[]): void {
|
|
22
|
+
for (const f of flags) this.flags.set(f.key, f);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Load remote config */
|
|
26
|
+
loadConfig(config: Record<string, unknown>): void {
|
|
27
|
+
for (const [k, v] of Object.entries(config)) this.config.set(k, v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Check if feature is enabled */
|
|
31
|
+
isEnabled(key: string): boolean {
|
|
32
|
+
const flag = this.flags.get(key);
|
|
33
|
+
if (!flag) return false;
|
|
34
|
+
if (!flag.enabled) return false;
|
|
35
|
+
if (flag.rolloutPercent !== undefined) {
|
|
36
|
+
const hash = this.hashUser(this.userId + key);
|
|
37
|
+
return hash < flag.rolloutPercent;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Get A/B variant */
|
|
43
|
+
getVariant(key: string): string | null {
|
|
44
|
+
return this.flags.get(key)?.variant ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Get config value */
|
|
48
|
+
getConfig<T = unknown>(key: string, defaultValue: T): T {
|
|
49
|
+
return (this.config.get(key) as T) ?? defaultValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Set user ID for consistent rollout */
|
|
53
|
+
setUserId(userId: string): void {
|
|
54
|
+
this.userId = userId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Get all flags */
|
|
58
|
+
getAllFlags(): FeatureFlag[] {
|
|
59
|
+
return Array.from(this.flags.values());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private hashUser(input: string): number {
|
|
63
|
+
let hash = 0;
|
|
64
|
+
for (let i = 0; i < input.length; i++) {
|
|
65
|
+
hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0;
|
|
66
|
+
}
|
|
67
|
+
return Math.abs(hash) % 100;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rajeev02/app-shell — Form Engine + KYC
|
|
3
|
+
* Dynamic forms, multi-step wizard, validation, Indian ID verification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type FieldType =
|
|
7
|
+
| "text"
|
|
8
|
+
| "email"
|
|
9
|
+
| "phone"
|
|
10
|
+
| "number"
|
|
11
|
+
| "password"
|
|
12
|
+
| "select"
|
|
13
|
+
| "multiselect"
|
|
14
|
+
| "checkbox"
|
|
15
|
+
| "radio"
|
|
16
|
+
| "date"
|
|
17
|
+
| "file"
|
|
18
|
+
| "image"
|
|
19
|
+
| "aadhaar"
|
|
20
|
+
| "pan"
|
|
21
|
+
| "ifsc"
|
|
22
|
+
| "pincode"
|
|
23
|
+
| "vpa"
|
|
24
|
+
| "textarea"
|
|
25
|
+
| "otp";
|
|
26
|
+
|
|
27
|
+
export interface FormField {
|
|
28
|
+
id: string;
|
|
29
|
+
type: FieldType;
|
|
30
|
+
label: string;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
required?: boolean;
|
|
33
|
+
defaultValue?: unknown;
|
|
34
|
+
validation?: ValidationRule[];
|
|
35
|
+
options?: { label: string; value: string }[];
|
|
36
|
+
maxSizeBytes?: number;
|
|
37
|
+
acceptedTypes?: string[];
|
|
38
|
+
dependsOn?: { fieldId: string; value: unknown };
|
|
39
|
+
helpText?: string;
|
|
40
|
+
masked?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ValidationRule {
|
|
44
|
+
type:
|
|
45
|
+
| "required"
|
|
46
|
+
| "minLength"
|
|
47
|
+
| "maxLength"
|
|
48
|
+
| "pattern"
|
|
49
|
+
| "email"
|
|
50
|
+
| "phone"
|
|
51
|
+
| "aadhaar"
|
|
52
|
+
| "pan"
|
|
53
|
+
| "ifsc"
|
|
54
|
+
| "pincode"
|
|
55
|
+
| "custom";
|
|
56
|
+
value?: unknown;
|
|
57
|
+
message: string;
|
|
58
|
+
validator?: (value: unknown) => boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FormStep {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
fields: FormField[];
|
|
66
|
+
}
|
|
67
|
+
export interface FormConfig {
|
|
68
|
+
id: string;
|
|
69
|
+
title: string;
|
|
70
|
+
steps: FormStep[];
|
|
71
|
+
onSubmit?: (
|
|
72
|
+
data: Record<string, unknown>,
|
|
73
|
+
) => Promise<{ success: boolean; error?: string }>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FormState {
|
|
77
|
+
currentStep: number;
|
|
78
|
+
totalSteps: number;
|
|
79
|
+
values: Record<string, unknown>;
|
|
80
|
+
errors: Record<string, string>;
|
|
81
|
+
touched: Record<string, boolean>;
|
|
82
|
+
isSubmitting: boolean;
|
|
83
|
+
isDirty: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class FormEngine {
|
|
87
|
+
private config: FormConfig;
|
|
88
|
+
private state: FormState;
|
|
89
|
+
|
|
90
|
+
constructor(config: FormConfig) {
|
|
91
|
+
this.config = config;
|
|
92
|
+
this.state = {
|
|
93
|
+
currentStep: 0,
|
|
94
|
+
totalSteps: config.steps.length,
|
|
95
|
+
values: {},
|
|
96
|
+
errors: {},
|
|
97
|
+
touched: {},
|
|
98
|
+
isSubmitting: false,
|
|
99
|
+
isDirty: false,
|
|
100
|
+
};
|
|
101
|
+
for (const step of config.steps)
|
|
102
|
+
for (const f of step.fields)
|
|
103
|
+
if (f.defaultValue !== undefined)
|
|
104
|
+
this.state.values[f.id] = f.defaultValue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setValue(fieldId: string, value: unknown): void {
|
|
108
|
+
this.state.values[fieldId] = value;
|
|
109
|
+
this.state.touched[fieldId] = true;
|
|
110
|
+
this.state.isDirty = true;
|
|
111
|
+
this.validateField(fieldId);
|
|
112
|
+
}
|
|
113
|
+
getValue(fieldId: string): unknown {
|
|
114
|
+
return this.state.values[fieldId];
|
|
115
|
+
}
|
|
116
|
+
getValues(): Record<string, unknown> {
|
|
117
|
+
return { ...this.state.values };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
validateField(fieldId: string): string | null {
|
|
121
|
+
const field = this.config.steps
|
|
122
|
+
.flatMap((s) => s.fields)
|
|
123
|
+
.find((f) => f.id === fieldId);
|
|
124
|
+
if (!field) return null;
|
|
125
|
+
const value = this.state.values[fieldId];
|
|
126
|
+
const str = String(value ?? "");
|
|
127
|
+
|
|
128
|
+
for (const rule of field.validation ?? []) {
|
|
129
|
+
let error: string | null = null;
|
|
130
|
+
switch (rule.type) {
|
|
131
|
+
case "required":
|
|
132
|
+
error = !value || str.trim() === "" ? rule.message : null;
|
|
133
|
+
break;
|
|
134
|
+
case "minLength":
|
|
135
|
+
error = str.length < (rule.value as number) ? rule.message : null;
|
|
136
|
+
break;
|
|
137
|
+
case "maxLength":
|
|
138
|
+
error = str.length > (rule.value as number) ? rule.message : null;
|
|
139
|
+
break;
|
|
140
|
+
case "pattern":
|
|
141
|
+
error = !new RegExp(rule.value as string).test(str)
|
|
142
|
+
? rule.message
|
|
143
|
+
: null;
|
|
144
|
+
break;
|
|
145
|
+
case "email":
|
|
146
|
+
error = !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str) ? rule.message : null;
|
|
147
|
+
break;
|
|
148
|
+
case "phone":
|
|
149
|
+
error = !/^[6-9]\d{9}$/.test(
|
|
150
|
+
str.replace(/\D/g, "").replace(/^91/, ""),
|
|
151
|
+
)
|
|
152
|
+
? rule.message
|
|
153
|
+
: null;
|
|
154
|
+
break;
|
|
155
|
+
case "aadhaar":
|
|
156
|
+
error = !/^[2-9]\d{11}$/.test(str.replace(/\s/g, ""))
|
|
157
|
+
? rule.message
|
|
158
|
+
: null;
|
|
159
|
+
break;
|
|
160
|
+
case "pan":
|
|
161
|
+
error = !/^[A-Z]{5}[0-9]{4}[A-Z]$/.test(str.toUpperCase())
|
|
162
|
+
? rule.message
|
|
163
|
+
: null;
|
|
164
|
+
break;
|
|
165
|
+
case "ifsc":
|
|
166
|
+
error = !/^[A-Z]{4}0[A-Z0-9]{6}$/.test(str.toUpperCase())
|
|
167
|
+
? rule.message
|
|
168
|
+
: null;
|
|
169
|
+
break;
|
|
170
|
+
case "pincode":
|
|
171
|
+
error = !/^\d{6}$/.test(str) ? rule.message : null;
|
|
172
|
+
break;
|
|
173
|
+
case "custom":
|
|
174
|
+
error =
|
|
175
|
+
rule.validator && !rule.validator(value) ? rule.message : null;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
if (error) {
|
|
179
|
+
this.state.errors[fieldId] = error;
|
|
180
|
+
return error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
delete this.state.errors[fieldId];
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
validateCurrentStep(): boolean {
|
|
188
|
+
const step = this.config.steps[this.state.currentStep];
|
|
189
|
+
let valid = true;
|
|
190
|
+
for (const f of step.fields) {
|
|
191
|
+
if (this.validateField(f.id)) valid = false;
|
|
192
|
+
}
|
|
193
|
+
return valid;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
nextStep(): boolean {
|
|
197
|
+
if (!this.validateCurrentStep()) return false;
|
|
198
|
+
if (this.state.currentStep < this.state.totalSteps - 1) {
|
|
199
|
+
this.state.currentStep++;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
prevStep(): boolean {
|
|
205
|
+
if (this.state.currentStep > 0) {
|
|
206
|
+
this.state.currentStep--;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async submit(): Promise<{ success: boolean; error?: string }> {
|
|
213
|
+
this.state.isSubmitting = true;
|
|
214
|
+
try {
|
|
215
|
+
if (this.config.onSubmit)
|
|
216
|
+
return await this.config.onSubmit(this.state.values);
|
|
217
|
+
return { success: true };
|
|
218
|
+
} finally {
|
|
219
|
+
this.state.isSubmitting = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getState(): FormState {
|
|
224
|
+
return { ...this.state };
|
|
225
|
+
}
|
|
226
|
+
getCurrentStep(): FormStep {
|
|
227
|
+
return this.config.steps[this.state.currentStep];
|
|
228
|
+
}
|
|
229
|
+
reset(): void {
|
|
230
|
+
this.state = {
|
|
231
|
+
currentStep: 0,
|
|
232
|
+
totalSteps: this.config.steps.length,
|
|
233
|
+
values: {},
|
|
234
|
+
errors: {},
|
|
235
|
+
touched: {},
|
|
236
|
+
isSubmitting: false,
|
|
237
|
+
isDirty: false,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Pre-built KYC form */
|
|
243
|
+
export function getKycFormConfig(): FormConfig {
|
|
244
|
+
return {
|
|
245
|
+
id: "kyc",
|
|
246
|
+
title: "Complete KYC",
|
|
247
|
+
steps: [
|
|
248
|
+
{
|
|
249
|
+
id: "personal",
|
|
250
|
+
title: "Personal Details",
|
|
251
|
+
fields: [
|
|
252
|
+
{
|
|
253
|
+
id: "fullName",
|
|
254
|
+
type: "text",
|
|
255
|
+
label: "Full Name (as per Aadhaar)",
|
|
256
|
+
required: true,
|
|
257
|
+
validation: [{ type: "required", message: "Name is required" }],
|
|
258
|
+
},
|
|
259
|
+
{ id: "dob", type: "date", label: "Date of Birth", required: true },
|
|
260
|
+
{
|
|
261
|
+
id: "phone",
|
|
262
|
+
type: "phone",
|
|
263
|
+
label: "Mobile Number",
|
|
264
|
+
required: true,
|
|
265
|
+
validation: [
|
|
266
|
+
{ type: "phone", message: "Enter valid mobile number" },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "email",
|
|
271
|
+
type: "email",
|
|
272
|
+
label: "Email",
|
|
273
|
+
validation: [{ type: "email", message: "Enter valid email" }],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "identity",
|
|
279
|
+
title: "Identity Verification",
|
|
280
|
+
fields: [
|
|
281
|
+
{
|
|
282
|
+
id: "aadhaar",
|
|
283
|
+
type: "aadhaar",
|
|
284
|
+
label: "Aadhaar Number",
|
|
285
|
+
required: true,
|
|
286
|
+
masked: true,
|
|
287
|
+
validation: [
|
|
288
|
+
{ type: "aadhaar", message: "Enter valid 12-digit Aadhaar" },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "pan",
|
|
293
|
+
type: "pan",
|
|
294
|
+
label: "PAN Number",
|
|
295
|
+
required: true,
|
|
296
|
+
validation: [
|
|
297
|
+
{ type: "pan", message: "Enter valid PAN (e.g., ABCDE1234F)" },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: "address",
|
|
304
|
+
title: "Address",
|
|
305
|
+
fields: [
|
|
306
|
+
{
|
|
307
|
+
id: "address1",
|
|
308
|
+
type: "text",
|
|
309
|
+
label: "Address Line 1",
|
|
310
|
+
required: true,
|
|
311
|
+
},
|
|
312
|
+
{ id: "city", type: "text", label: "City", required: true },
|
|
313
|
+
{
|
|
314
|
+
id: "state",
|
|
315
|
+
type: "select",
|
|
316
|
+
label: "State",
|
|
317
|
+
required: true,
|
|
318
|
+
options: [
|
|
319
|
+
{ label: "Maharashtra", value: "MH" },
|
|
320
|
+
{ label: "Karnataka", value: "KA" },
|
|
321
|
+
{ label: "Tamil Nadu", value: "TN" },
|
|
322
|
+
{ label: "Delhi", value: "DL" },
|
|
323
|
+
{ label: "West Bengal", value: "WB" },
|
|
324
|
+
{ label: "Uttar Pradesh", value: "UP" },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "pincode",
|
|
329
|
+
type: "pincode",
|
|
330
|
+
label: "PIN Code",
|
|
331
|
+
required: true,
|
|
332
|
+
validation: [
|
|
333
|
+
{ type: "pincode", message: "Enter valid 6-digit PIN" },
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: "documents",
|
|
340
|
+
title: "Upload Documents",
|
|
341
|
+
fields: [
|
|
342
|
+
{
|
|
343
|
+
id: "aadhaarFront",
|
|
344
|
+
type: "image",
|
|
345
|
+
label: "Aadhaar Card (Front)",
|
|
346
|
+
required: true,
|
|
347
|
+
maxSizeBytes: 5242880,
|
|
348
|
+
acceptedTypes: ["image/jpeg", "image/png"],
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: "aadhaarBack",
|
|
352
|
+
type: "image",
|
|
353
|
+
label: "Aadhaar Card (Back)",
|
|
354
|
+
required: true,
|
|
355
|
+
maxSizeBytes: 5242880,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "panPhoto",
|
|
359
|
+
type: "image",
|
|
360
|
+
label: "PAN Card Photo",
|
|
361
|
+
required: true,
|
|
362
|
+
maxSizeBytes: 5242880,
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: "selfie",
|
|
366
|
+
type: "image",
|
|
367
|
+
label: "Live Selfie",
|
|
368
|
+
required: true,
|
|
369
|
+
maxSizeBytes: 5242880,
|
|
370
|
+
helpText: "Take a clear selfie for face verification",
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
}
|