@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.
Files changed (41) hide show
  1. package/lib/analytics/index.d.ts +42 -0
  2. package/lib/analytics/index.d.ts.map +1 -0
  3. package/lib/analytics/index.js +69 -0
  4. package/lib/analytics/index.js.map +1 -0
  5. package/lib/api/index.d.ts +84 -0
  6. package/lib/api/index.d.ts.map +1 -0
  7. package/lib/api/index.js +219 -0
  8. package/lib/api/index.js.map +1 -0
  9. package/lib/cart/index.d.ts +97 -0
  10. package/lib/cart/index.d.ts.map +1 -0
  11. package/lib/cart/index.js +134 -0
  12. package/lib/cart/index.js.map +1 -0
  13. package/lib/chat/index.d.ts +111 -0
  14. package/lib/chat/index.d.ts.map +1 -0
  15. package/lib/chat/index.js +169 -0
  16. package/lib/chat/index.js.map +1 -0
  17. package/lib/config/index.d.ts +33 -0
  18. package/lib/config/index.d.ts.map +1 -0
  19. package/lib/config/index.js +62 -0
  20. package/lib/config/index.js.map +1 -0
  21. package/lib/forms/index.d.ts +78 -0
  22. package/lib/forms/index.d.ts.map +1 -0
  23. package/lib/forms/index.js +292 -0
  24. package/lib/forms/index.js.map +1 -0
  25. package/lib/index.d.ts +23 -0
  26. package/lib/index.d.ts.map +1 -0
  27. package/lib/index.js +35 -0
  28. package/lib/index.js.map +1 -0
  29. package/lib/onboarding/index.d.ts +62 -0
  30. package/lib/onboarding/index.d.ts.map +1 -0
  31. package/lib/onboarding/index.js +117 -0
  32. package/lib/onboarding/index.js.map +1 -0
  33. package/package.json +51 -0
  34. package/src/analytics/index.ts +92 -0
  35. package/src/api/index.ts +322 -0
  36. package/src/cart/index.ts +213 -0
  37. package/src/chat/index.ts +260 -0
  38. package/src/config/index.ts +69 -0
  39. package/src/forms/index.ts +376 -0
  40. package/src/index.ts +68 -0
  41. package/src/onboarding/index.ts +159 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @rajeev02/app-shell — Analytics Pipeline
3
+ * Event tracking, user properties, funnel analysis, offline buffer
4
+ */
5
+
6
+ export interface AnalyticsEvent {
7
+ name: string;
8
+ properties?: Record<string, unknown>;
9
+ timestamp: number;
10
+ sessionId: string;
11
+ }
12
+
13
+ export interface AnalyticsConfig {
14
+ /** Flush events to server handler */
15
+ onFlush: (events: AnalyticsEvent[]) => Promise<void>;
16
+ /** Flush interval in ms (default: 30000) */
17
+ flushIntervalMs?: number;
18
+ /** Max batch size (default: 50) */
19
+ maxBatchSize?: number;
20
+ /** Whether to track automatically (screen views, etc.) */
21
+ autoTrack?: boolean;
22
+ }
23
+
24
+ export class AnalyticsEngine {
25
+ private config: AnalyticsConfig;
26
+ private buffer: AnalyticsEvent[] = [];
27
+ private userProperties: Record<string, unknown> = {};
28
+ private sessionId: string;
29
+ private flushTimer: ReturnType<typeof setInterval> | null = null;
30
+
31
+ constructor(config: AnalyticsConfig) {
32
+ this.config = {
33
+ flushIntervalMs: 30000,
34
+ maxBatchSize: 50,
35
+ autoTrack: true,
36
+ ...config,
37
+ };
38
+ this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
39
+ this.startFlushTimer();
40
+ }
41
+
42
+ /** Track an event */
43
+ track(name: string, properties?: Record<string, unknown>): void {
44
+ this.buffer.push({
45
+ name,
46
+ properties: { ...this.userProperties, ...properties },
47
+ timestamp: Date.now(),
48
+ sessionId: this.sessionId,
49
+ });
50
+ if (this.buffer.length >= (this.config.maxBatchSize ?? 50)) this.flush();
51
+ }
52
+
53
+ /** Set user properties (included in all future events) */
54
+ setUserProperties(props: Record<string, unknown>): void {
55
+ Object.assign(this.userProperties, props);
56
+ }
57
+
58
+ /** Track screen view */
59
+ trackScreen(screenName: string, properties?: Record<string, unknown>): void {
60
+ this.track("screen_view", { screen_name: screenName, ...properties });
61
+ }
62
+
63
+ /** Flush events to server */
64
+ async flush(): Promise<void> {
65
+ if (this.buffer.length === 0) return;
66
+ const batch = [...this.buffer];
67
+ this.buffer = [];
68
+ try {
69
+ await this.config.onFlush(batch);
70
+ } catch {
71
+ this.buffer = [...batch, ...this.buffer];
72
+ }
73
+ }
74
+
75
+ /** Get buffer size */
76
+ getBufferSize(): number {
77
+ return this.buffer.length;
78
+ }
79
+
80
+ /** Destroy — flush and stop timer */
81
+ async destroy(): Promise<void> {
82
+ if (this.flushTimer) clearInterval(this.flushTimer);
83
+ await this.flush();
84
+ }
85
+
86
+ private startFlushTimer(): void {
87
+ this.flushTimer = setInterval(
88
+ () => this.flush(),
89
+ this.config.flushIntervalMs ?? 30000,
90
+ );
91
+ }
92
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * @rajeev02/app-shell — API Client
3
+ * Offline-first API layer with interceptors, token refresh, retry, cache-first strategy
4
+ */
5
+
6
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
7
+ export type CacheStrategy =
8
+ | "network_first"
9
+ | "cache_first"
10
+ | "network_only"
11
+ | "cache_only"
12
+ | "stale_while_revalidate";
13
+
14
+ export interface ApiConfig {
15
+ baseUrl: string;
16
+ timeout?: number;
17
+ defaultHeaders?: Record<string, string>;
18
+ /** Get current access token */
19
+ getToken?: () => Promise<string | null>;
20
+ /** Refresh token when 401 received */
21
+ onRefreshToken?: () => Promise<string | null>;
22
+ /** Called on network error (for queueing) */
23
+ onNetworkError?: (request: ApiRequest) => void;
24
+ /** Default cache strategy */
25
+ defaultCacheStrategy?: CacheStrategy;
26
+ /** Default cache TTL in seconds */
27
+ defaultCacheTtl?: number;
28
+ }
29
+
30
+ export interface ApiRequest {
31
+ method: HttpMethod;
32
+ path: string;
33
+ body?: unknown;
34
+ headers?: Record<string, string>;
35
+ query?: Record<string, string>;
36
+ cacheStrategy?: CacheStrategy;
37
+ cacheTtl?: number;
38
+ retryCount?: number;
39
+ maxRetries?: number;
40
+ tag?: string;
41
+ }
42
+
43
+ export interface ApiResponse<T = unknown> {
44
+ data: T;
45
+ status: number;
46
+ headers: Record<string, string>;
47
+ fromCache: boolean;
48
+ duration: number;
49
+ }
50
+
51
+ export interface RequestInterceptor {
52
+ name: string;
53
+ onRequest: (request: ApiRequest) => ApiRequest | Promise<ApiRequest>;
54
+ }
55
+
56
+ export interface ResponseInterceptor {
57
+ name: string;
58
+ onResponse: (response: ApiResponse) => ApiResponse | Promise<ApiResponse>;
59
+ onError?: (error: unknown, request: ApiRequest) => unknown;
60
+ }
61
+
62
+ /**
63
+ * Offline-First API Client
64
+ */
65
+ export class ApiClient {
66
+ private config: ApiConfig;
67
+ private requestInterceptors: RequestInterceptor[] = [];
68
+ private responseInterceptors: ResponseInterceptor[] = [];
69
+ private pendingRequests: Map<string, ApiRequest> = new Map();
70
+ private cache: Map<string, { response: ApiResponse; expiresAt: number }> =
71
+ new Map();
72
+ private isRefreshing: boolean = false;
73
+ private refreshQueue: ((token: string | null) => void)[] = [];
74
+
75
+ constructor(config: ApiConfig) {
76
+ this.config = {
77
+ timeout: 15000,
78
+ defaultCacheStrategy: "network_first",
79
+ defaultCacheTtl: 300,
80
+ ...config,
81
+ };
82
+ }
83
+
84
+ /** Add request interceptor */
85
+ addRequestInterceptor(interceptor: RequestInterceptor): void {
86
+ this.requestInterceptors.push(interceptor);
87
+ }
88
+
89
+ /** Add response interceptor */
90
+ addResponseInterceptor(interceptor: ResponseInterceptor): void {
91
+ this.responseInterceptors.push(interceptor);
92
+ }
93
+
94
+ /** Main request method */
95
+ async request<T = unknown>(req: ApiRequest): Promise<ApiResponse<T>> {
96
+ let request = {
97
+ ...req,
98
+ retryCount: req.retryCount ?? 0,
99
+ maxRetries: req.maxRetries ?? 2,
100
+ };
101
+ const strategy =
102
+ request.cacheStrategy ??
103
+ this.config.defaultCacheStrategy ??
104
+ "network_first";
105
+ const cacheKey = this.getCacheKey(request);
106
+
107
+ // Run request interceptors
108
+ for (const interceptor of this.requestInterceptors) {
109
+ const intercepted = await interceptor.onRequest(request);
110
+ request = {
111
+ ...intercepted,
112
+ retryCount: intercepted.retryCount ?? request.retryCount,
113
+ maxRetries: intercepted.maxRetries ?? request.maxRetries,
114
+ };
115
+ }
116
+
117
+ // Cache strategies
118
+ if (strategy === "cache_only" || strategy === "cache_first") {
119
+ const cached = this.getFromCache<T>(cacheKey);
120
+ if (cached) return cached;
121
+ if (strategy === "cache_only") {
122
+ throw new Error("No cached response available");
123
+ }
124
+ }
125
+
126
+ if (strategy === "stale_while_revalidate") {
127
+ const cached = this.getFromCache<T>(cacheKey);
128
+ // Return stale cache immediately, revalidate in background
129
+ if (cached) {
130
+ this.fetchFromNetwork<T>(request, cacheKey).catch(() => {});
131
+ return cached;
132
+ }
133
+ }
134
+
135
+ try {
136
+ const response = await this.fetchFromNetwork<T>(request, cacheKey);
137
+ return response;
138
+ } catch (error) {
139
+ // On network error, try cache as fallback
140
+ if (strategy === "network_first") {
141
+ const cached = this.getFromCache<T>(cacheKey);
142
+ if (cached) return cached;
143
+ }
144
+ // Queue for later if offline
145
+ if (this.config.onNetworkError && request.method !== "GET") {
146
+ this.config.onNetworkError(request);
147
+ }
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ // Convenience methods
153
+ async get<T = unknown>(
154
+ path: string,
155
+ query?: Record<string, string>,
156
+ options?: Partial<ApiRequest>,
157
+ ): Promise<ApiResponse<T>> {
158
+ return this.request<T>({ method: "GET", path, query, ...options });
159
+ }
160
+ async post<T = unknown>(
161
+ path: string,
162
+ body?: unknown,
163
+ options?: Partial<ApiRequest>,
164
+ ): Promise<ApiResponse<T>> {
165
+ return this.request<T>({ method: "POST", path, body, ...options });
166
+ }
167
+ async put<T = unknown>(
168
+ path: string,
169
+ body?: unknown,
170
+ options?: Partial<ApiRequest>,
171
+ ): Promise<ApiResponse<T>> {
172
+ return this.request<T>({ method: "PUT", path, body, ...options });
173
+ }
174
+ async patch<T = unknown>(
175
+ path: string,
176
+ body?: unknown,
177
+ options?: Partial<ApiRequest>,
178
+ ): Promise<ApiResponse<T>> {
179
+ return this.request<T>({ method: "PATCH", path, body, ...options });
180
+ }
181
+ async delete<T = unknown>(
182
+ path: string,
183
+ options?: Partial<ApiRequest>,
184
+ ): Promise<ApiResponse<T>> {
185
+ return this.request<T>({ method: "DELETE", path, ...options });
186
+ }
187
+
188
+ /** Clear API cache */
189
+ clearCache(): void {
190
+ this.cache.clear();
191
+ }
192
+
193
+ /** Get pending (queued) requests count */
194
+ getPendingCount(): number {
195
+ return this.pendingRequests.size;
196
+ }
197
+
198
+ private async fetchFromNetwork<T>(
199
+ request: ApiRequest,
200
+ cacheKey: string,
201
+ ): Promise<ApiResponse<T>> {
202
+ const start = Date.now();
203
+ const url = this.buildUrl(request);
204
+ const headers: Record<string, string> = {
205
+ "Content-Type": "application/json",
206
+ ...this.config.defaultHeaders,
207
+ ...request.headers,
208
+ };
209
+
210
+ // Add auth token
211
+ if (this.config.getToken) {
212
+ const token = await this.config.getToken();
213
+ if (token) headers["Authorization"] = `Bearer ${token}`;
214
+ }
215
+
216
+ const fetchOptions: RequestInit = { method: request.method, headers };
217
+ if (request.body && request.method !== "GET") {
218
+ fetchOptions.body = JSON.stringify(request.body);
219
+ }
220
+
221
+ const httpResponse = await fetch(url, fetchOptions);
222
+
223
+ // Handle 401 — token expired
224
+ if (httpResponse.status === 401 && this.config.onRefreshToken) {
225
+ const newToken = await this.handleTokenRefresh();
226
+ if (newToken) {
227
+ headers["Authorization"] = `Bearer ${newToken}`;
228
+ const retryResponse = await fetch(url, { ...fetchOptions, headers });
229
+ return this.processResponse<T>(retryResponse, cacheKey, start);
230
+ }
231
+ }
232
+
233
+ return this.processResponse<T>(httpResponse, cacheKey, start);
234
+ }
235
+
236
+ private async processResponse<T>(
237
+ httpResponse: Response,
238
+ cacheKey: string,
239
+ start: number,
240
+ ): Promise<ApiResponse<T>> {
241
+ const data = (await httpResponse.json()) as T;
242
+ const responseHeaders: Record<string, string> = {};
243
+ httpResponse.headers.forEach((v, k) => {
244
+ responseHeaders[k] = v;
245
+ });
246
+
247
+ let response: ApiResponse<T> = {
248
+ data,
249
+ status: httpResponse.status,
250
+ headers: responseHeaders,
251
+ fromCache: false,
252
+ duration: Date.now() - start,
253
+ };
254
+
255
+ // Run response interceptors
256
+ for (const interceptor of this.responseInterceptors) {
257
+ response = (await interceptor.onResponse(
258
+ response as ApiResponse,
259
+ )) as ApiResponse<T>;
260
+ }
261
+
262
+ // Cache successful GET responses
263
+ if (httpResponse.ok) {
264
+ const ttl = this.config.defaultCacheTtl ?? 300;
265
+ this.cache.set(cacheKey, {
266
+ response: response as ApiResponse,
267
+ expiresAt: Date.now() + ttl * 1000,
268
+ });
269
+ }
270
+
271
+ if (!httpResponse.ok) {
272
+ throw {
273
+ status: httpResponse.status,
274
+ data,
275
+ message: `HTTP ${httpResponse.status}`,
276
+ };
277
+ }
278
+
279
+ return response;
280
+ }
281
+
282
+ private async handleTokenRefresh(): Promise<string | null> {
283
+ if (this.isRefreshing) {
284
+ return new Promise((resolve) => {
285
+ this.refreshQueue.push(resolve);
286
+ });
287
+ }
288
+ this.isRefreshing = true;
289
+ try {
290
+ const token = await this.config.onRefreshToken!();
291
+ this.refreshQueue.forEach((cb) => cb(token));
292
+ this.refreshQueue = [];
293
+ return token;
294
+ } finally {
295
+ this.isRefreshing = false;
296
+ }
297
+ }
298
+
299
+ private getFromCache<T>(key: string): ApiResponse<T> | null {
300
+ const entry = this.cache.get(key);
301
+ if (!entry) return null;
302
+ if (entry.expiresAt < Date.now()) {
303
+ this.cache.delete(key);
304
+ return null;
305
+ }
306
+ return { ...entry.response, fromCache: true } as ApiResponse<T>;
307
+ }
308
+
309
+ private getCacheKey(req: ApiRequest): string {
310
+ const queryStr = req.query ? JSON.stringify(req.query) : "";
311
+ return `${req.method}:${req.path}:${queryStr}`;
312
+ }
313
+
314
+ private buildUrl(req: ApiRequest): string {
315
+ let url = `${this.config.baseUrl}${req.path}`;
316
+ if (req.query) {
317
+ const params = new URLSearchParams(req.query).toString();
318
+ if (params) url += `?${params}`;
319
+ }
320
+ return url;
321
+ }
322
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @rajeev02/app-shell — Cart & Checkout
3
+ * Cart management, address, coupons, order tracking
4
+ */
5
+
6
+ export interface CartItem {
7
+ id: string;
8
+ productId: string;
9
+ name: string;
10
+ imageUrl?: string;
11
+ price: number;
12
+ quantity: number;
13
+ maxQuantity?: number;
14
+ variant?: string;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface CartSummary {
19
+ items: CartItem[];
20
+ subtotal: number;
21
+ discount: number;
22
+ tax: number;
23
+ deliveryFee: number;
24
+ total: number;
25
+ couponCode?: string;
26
+ itemCount: number;
27
+ }
28
+
29
+ export interface Address {
30
+ id: string;
31
+ label?: string;
32
+ fullName: string;
33
+ phone: string;
34
+ line1: string;
35
+ line2?: string;
36
+ city: string;
37
+ state: string;
38
+ pincode: string;
39
+ isDefault: boolean;
40
+ latitude?: number;
41
+ longitude?: number;
42
+ }
43
+
44
+ export type OrderStatus =
45
+ | "created"
46
+ | "confirmed"
47
+ | "processing"
48
+ | "shipped"
49
+ | "out_for_delivery"
50
+ | "delivered"
51
+ | "cancelled"
52
+ | "returned"
53
+ | "refunded";
54
+
55
+ export interface Order {
56
+ id: string;
57
+ items: CartItem[];
58
+ address: Address;
59
+ status: OrderStatus;
60
+ paymentMethod: string;
61
+ paymentStatus: "pending" | "paid" | "failed" | "refunded";
62
+ subtotal: number;
63
+ discount: number;
64
+ tax: number;
65
+ deliveryFee: number;
66
+ total: number;
67
+ couponCode?: string;
68
+ trackingId?: string;
69
+ createdAt: number;
70
+ updatedAt: number;
71
+ estimatedDelivery?: string;
72
+ }
73
+
74
+ /**
75
+ * Cart Manager
76
+ */
77
+ export class CartManager {
78
+ private items: Map<string, CartItem> = new Map();
79
+ private couponCode: string | null = null;
80
+ private couponDiscount: number = 0;
81
+ private taxRate: number = 0.18; // 18% GST
82
+ private deliveryFee: number = 0;
83
+ private listeners: Set<() => void> = new Set();
84
+
85
+ /** Add item to cart */
86
+ add(item: Omit<CartItem, "quantity">, quantity: number = 1): void {
87
+ const existing = this.items.get(item.id);
88
+ if (existing) {
89
+ existing.quantity = Math.min(
90
+ existing.quantity + quantity,
91
+ existing.maxQuantity ?? 99,
92
+ );
93
+ } else {
94
+ this.items.set(item.id, { ...item, quantity });
95
+ }
96
+ this.notify();
97
+ }
98
+
99
+ /** Update quantity */
100
+ updateQuantity(itemId: string, quantity: number): void {
101
+ const item = this.items.get(itemId);
102
+ if (item) {
103
+ if (quantity <= 0) {
104
+ this.items.delete(itemId);
105
+ } else {
106
+ item.quantity = Math.min(quantity, item.maxQuantity ?? 99);
107
+ }
108
+ this.notify();
109
+ }
110
+ }
111
+
112
+ /** Remove item */
113
+ remove(itemId: string): void {
114
+ this.items.delete(itemId);
115
+ this.notify();
116
+ }
117
+
118
+ /** Clear cart */
119
+ clear(): void {
120
+ this.items.clear();
121
+ this.couponCode = null;
122
+ this.couponDiscount = 0;
123
+ this.notify();
124
+ }
125
+
126
+ /** Apply coupon */
127
+ applyCoupon(code: string, discountAmount: number): void {
128
+ this.couponCode = code;
129
+ this.couponDiscount = discountAmount;
130
+ this.notify();
131
+ }
132
+
133
+ /** Remove coupon */
134
+ removeCoupon(): void {
135
+ this.couponCode = null;
136
+ this.couponDiscount = 0;
137
+ this.notify();
138
+ }
139
+
140
+ /** Set delivery fee */
141
+ setDeliveryFee(fee: number): void {
142
+ this.deliveryFee = fee;
143
+ }
144
+
145
+ /** Get cart summary */
146
+ getSummary(): CartSummary {
147
+ const items = Array.from(this.items.values());
148
+ const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
149
+ const discount = this.couponDiscount;
150
+ const taxable = subtotal - discount;
151
+ const tax = Math.max(0, taxable * this.taxRate);
152
+ const total = Math.max(0, taxable + tax + this.deliveryFee);
153
+
154
+ return {
155
+ items,
156
+ subtotal,
157
+ discount,
158
+ tax,
159
+ deliveryFee: this.deliveryFee,
160
+ total,
161
+ couponCode: this.couponCode ?? undefined,
162
+ itemCount: items.reduce((sum, i) => sum + i.quantity, 0),
163
+ };
164
+ }
165
+
166
+ /** Check if cart is empty */
167
+ isEmpty(): boolean {
168
+ return this.items.size === 0;
169
+ }
170
+
171
+ /** Get item count */
172
+ getItemCount(): number {
173
+ return Array.from(this.items.values()).reduce(
174
+ (sum, i) => sum + i.quantity,
175
+ 0,
176
+ );
177
+ }
178
+
179
+ /** Subscribe to cart changes */
180
+ onChange(listener: () => void): () => void {
181
+ this.listeners.add(listener);
182
+ return () => this.listeners.delete(listener);
183
+ }
184
+
185
+ /** Export cart for persistence */
186
+ export(): Record<string, unknown> {
187
+ return {
188
+ items: Array.from(this.items.values()),
189
+ couponCode: this.couponCode,
190
+ couponDiscount: this.couponDiscount,
191
+ };
192
+ }
193
+
194
+ /** Import cart from persistence */
195
+ import(data: Record<string, unknown>): void {
196
+ if (Array.isArray(data.items)) {
197
+ for (const item of data.items as CartItem[]) {
198
+ this.items.set(item.id, item);
199
+ }
200
+ }
201
+ if (typeof data.couponCode === "string") this.couponCode = data.couponCode;
202
+ if (typeof data.couponDiscount === "number")
203
+ this.couponDiscount = data.couponDiscount;
204
+ }
205
+
206
+ private notify(): void {
207
+ for (const l of this.listeners) {
208
+ try {
209
+ l();
210
+ } catch {}
211
+ }
212
+ }
213
+ }