@kasuvia/sdk 0.0.1 → 0.0.4

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 (69) hide show
  1. package/README.md +1 -1
  2. package/dist/hooks/index.d.mts +429 -0
  3. package/dist/hooks/index.d.ts +429 -0
  4. package/dist/hooks/index.js +3073 -0
  5. package/dist/hooks/index.js.map +1 -0
  6. package/dist/hooks/index.mjs +2962 -0
  7. package/dist/hooks/index.mjs.map +1 -0
  8. package/dist/index.d.mts +279 -1152
  9. package/dist/index.d.ts +279 -1152
  10. package/dist/index.js +1471 -2208
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1451 -2107
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/sdk-context-Cy51kEKT.d.mts +565 -0
  15. package/dist/sdk-context-Cy51kEKT.d.ts +565 -0
  16. package/package.json +6 -2
  17. package/src/config/index.ts +10 -91
  18. package/src/constants.ts +13 -0
  19. package/src/context/sdk-context.tsx +70 -0
  20. package/src/graphql/collections.graphql.ts +2 -10
  21. package/src/graphql/products.graphql.ts +215 -65
  22. package/src/graphql/wishlist.graphql.ts +7 -6
  23. package/src/hooks/index.ts +2 -0
  24. package/src/hooks/use-auth.ts +82 -158
  25. package/src/hooks/use-bespoke-availability.ts +25 -8
  26. package/src/hooks/use-cart.ts +222 -119
  27. package/src/hooks/use-cms.ts +179 -86
  28. package/src/hooks/use-collections.ts +52 -15
  29. package/src/hooks/use-consultation-stream.ts +4 -3
  30. package/src/hooks/use-consultations.ts +120 -51
  31. package/src/hooks/use-countries.ts +35 -13
  32. package/src/hooks/use-currencies.ts +26 -13
  33. package/src/hooks/use-google-translate.ts +22 -8
  34. package/src/hooks/use-languages.ts +40 -20
  35. package/src/hooks/use-mobile.ts +23 -0
  36. package/src/hooks/use-order-stream.ts +9 -5
  37. package/src/hooks/use-orders.ts +99 -44
  38. package/src/hooks/use-payment.ts +108 -40
  39. package/src/hooks/use-products.ts +251 -53
  40. package/src/hooks/use-service-booking.ts +152 -90
  41. package/src/hooks/use-states.ts +35 -15
  42. package/src/hooks/use-wishlist.ts +92 -91
  43. package/src/index.ts +27 -28
  44. package/src/services/auth.service.ts +209 -167
  45. package/src/services/bespoke.service.ts +12 -4
  46. package/src/services/cart.service.ts +172 -70
  47. package/src/services/cms.service.ts +70 -35
  48. package/src/services/collection.service.ts +22 -5
  49. package/src/services/config.service.ts +34 -14
  50. package/src/services/consultation.service.ts +72 -18
  51. package/src/services/logistics.service.ts +35 -11
  52. package/src/services/notifications.service.ts +27 -12
  53. package/src/services/order.service.ts +74 -15
  54. package/src/services/payment.service.ts +86 -31
  55. package/src/services/product.service.ts +279 -39
  56. package/src/services/service-booking.service.ts +98 -26
  57. package/src/services/wishlist.service.ts +69 -25
  58. package/src/types/auth/index.ts +27 -4
  59. package/src/types/business/index.ts +7 -2
  60. package/src/types/payments/index.ts +3 -3
  61. package/src/types/services/index.ts +2 -5
  62. package/src/types/store/index.ts +44 -5
  63. package/src/utils/customer.ts +14 -0
  64. package/src/utils/error-handler.ts +10 -5
  65. package/src/utils/storage.ts +93 -0
  66. package/src/utils/validators.ts +85 -0
  67. package/src/client/authClient.ts +0 -135
  68. package/src/client/graphqlClient.ts +0 -53
  69. package/src/client/httpClient.ts +0 -298
@@ -2,14 +2,15 @@
2
2
  * Wishlist Service
3
3
  *
4
4
  * Handles wishlist operations.
5
+ * Stateless - all configuration passed as parameters.
5
6
  */
6
- import { storeManagementAxios } from '../client/httpClient'
7
- import { getGraphQLClient } from '../client/graphqlClient'
7
+ import { GraphQLClient } from 'graphql-request'
8
+ import type { SdkConfig } from '../config'
8
9
  import type { WishlistItem, WishlistListResponse, WishlistCheckResponse } from '../types/store'
9
10
 
10
- const GET_WISHLIST = `
11
- query GetWishlist($page: Int!, $pageSize: Int!) {
12
- wishlist(page: $page, pageSize: $pageSize) {
11
+ const GET_STOREFRONT_WISHLIST = `
12
+ query GetStorefrontWishlist($page: Int!, $pageSize: Int!) {
13
+ storefront_wishlist(page: $page, pageSize: $pageSize) {
13
14
  items {
14
15
  id
15
16
  product_id
@@ -18,6 +19,7 @@ const GET_WISHLIST = `
18
19
  product_name
19
20
  product_slug
20
21
  product_price
22
+ currency
21
23
  product_image
22
24
  }
23
25
  total
@@ -28,57 +30,99 @@ const GET_WISHLIST = `
28
30
  }
29
31
  `
30
32
 
31
- const CHECK_IN_WISHLIST = `
32
- query CheckInWishlist($productId: String!) {
33
- check_in_wishlist(productId: $productId) {
33
+ const CHECK_IN_STOREFRONT_WISHLIST = `
34
+ query CheckInStorefrontWishlist($productId: String!) {
35
+ check_in_storefront_wishlist(productId: $productId) {
34
36
  in_wishlist
35
37
  }
36
38
  }
37
39
  `
38
40
 
41
+ /**
42
+ * Create GraphQL client with provided config
43
+ */
44
+ function createGraphQLClient(config: SdkConfig, accessToken?: string): GraphQLClient {
45
+ const headers: Record<string, string> = {
46
+ 'Content-Type': 'application/json',
47
+ 'Accept': 'application/json',
48
+ 'X-Storefront-Key': config.storefrontApiKey,
49
+ 'X-Business-ID': config.businessId,
50
+ }
51
+ if (accessToken) {
52
+ headers['Authorization'] = `Bearer ${accessToken}`
53
+ }
54
+ return new GraphQLClient(`${config.backendUrls.store}/graphql`, { headers })
55
+ }
56
+
39
57
  /**
40
58
  * Fetch the customer's wishlist for the current business.
41
59
  */
42
- export async function getWishlist(params?: { page?: number; pageSize?: number }): Promise<WishlistListResponse> {
43
- const client = getGraphQLClient()
60
+ export async function getWishlist(config: SdkConfig, accessToken: string, params?: { page?: number; pageSize?: number }): Promise<WishlistListResponse> {
61
+ const client = createGraphQLClient(config, accessToken)
44
62
  const variables = {
45
63
  page: params?.page || 1,
46
64
  pageSize: params?.pageSize || 20,
47
65
  }
48
- const data = await client.request(GET_WISHLIST, variables)
49
- return data.wishlist
66
+ const data = await client.request(GET_STOREFRONT_WISHLIST, variables)
67
+ return data.storefront_wishlist
50
68
  }
51
69
 
52
70
  /**
53
71
  * Check if a product is in the customer's wishlist.
54
72
  */
55
- export async function checkInWishlist(productId: string): Promise<WishlistCheckResponse> {
56
- const client = getGraphQLClient()
57
- const data = await client.request(CHECK_IN_WISHLIST, { productId })
58
- return data.check_in_wishlist
73
+ export async function checkInWishlist(config: SdkConfig, accessToken: string, productId: string): Promise<WishlistCheckResponse> {
74
+ const client = createGraphQLClient(config, accessToken)
75
+ const data = await client.request(CHECK_IN_STOREFRONT_WISHLIST, { productId })
76
+ return data.check_in_storefront_wishlist
59
77
  }
60
78
 
61
79
  /**
62
80
  * Add a product to the customer's wishlist.
63
81
  */
64
- export async function addToWishlist(productId: string, notes?: string): Promise<WishlistItem> {
65
- const { data } = await storeManagementAxios.post<WishlistItem>("wishlist/", {
66
- product_id: productId,
67
- notes: notes || undefined,
82
+ export async function addToWishlist(config: SdkConfig, accessToken: string, productId: string, notes?: string): Promise<WishlistItem> {
83
+ const response = await fetch(`${config.backendUrls.store}/wishlist/`, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ 'Authorization': `Bearer ${accessToken}`,
88
+ "X-Storefront-Key": config.storefrontApiKey,
89
+ "X-Business-ID": config.businessId,
90
+ },
91
+ body: JSON.stringify({
92
+ product_id: productId,
93
+ notes: notes || undefined,
94
+ }),
68
95
  })
69
- return data
96
+ if (!response.ok) throw new Error('Failed to add to wishlist')
97
+ return response.json()
70
98
  }
71
99
 
72
100
  /**
73
101
  * Remove an item from the customer's wishlist by item ID.
74
102
  */
75
- export async function removeFromWishlist(itemId: string): Promise<void> {
76
- await storeManagementAxios.delete(`wishlist/${itemId}`)
103
+ export async function removeFromWishlist(config: SdkConfig, accessToken: string, itemId: string): Promise<void> {
104
+ const response = await fetch(`${config.backendUrls.store}/wishlist/${itemId}`, {
105
+ method: 'DELETE',
106
+ headers: {
107
+ 'Authorization': `Bearer ${accessToken}`,
108
+ "X-Storefront-Key": config.storefrontApiKey,
109
+ "X-Business-ID": config.businessId,
110
+ },
111
+ })
112
+ if (!response.ok) throw new Error('Failed to remove from wishlist')
77
113
  }
78
114
 
79
115
  /**
80
116
  * Remove a product from the customer's wishlist by product ID.
81
117
  */
82
- export async function removeProductFromWishlist(productId: string): Promise<void> {
83
- await storeManagementAxios.delete(`wishlist/product/${productId}`)
118
+ export async function removeProductFromWishlist(config: SdkConfig, accessToken: string, productId: string): Promise<void> {
119
+ const response = await fetch(`${config.backendUrls.store}/wishlist/product/${productId}`, {
120
+ method: 'DELETE',
121
+ headers: {
122
+ 'Authorization': `Bearer ${accessToken}`,
123
+ "X-Storefront-Key": config.storefrontApiKey,
124
+ "X-Business-ID": config.businessId,
125
+ },
126
+ })
127
+ if (!response.ok) throw new Error('Failed to remove product from wishlist')
84
128
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Simplified customer auth types for luxe-storefront.
2
+ * Simplified customer auth types for kasuvia-storefront.
3
3
  * Customers only need enough to track their orders — no RBAC, MFA, or complex profiles.
4
4
  */
5
5
 
@@ -33,7 +33,7 @@ export interface CustomerUser {
33
33
  provider: 'google' | 'facebook' | 'email';
34
34
  }
35
35
 
36
- /** GET/PUT /auth/profile/ — matches luxe-auth UserSerializer */
36
+ /** GET/PUT /auth/profile/ — matches kasuvia-auth UserSerializer */
37
37
  export interface AuthUserResponse {
38
38
  id: string;
39
39
  email: string;
@@ -53,6 +53,27 @@ export interface UpdateProfilePayload {
53
53
  phone?: string;
54
54
  }
55
55
 
56
+ export interface NotificationPreferences {
57
+ email_enabled: boolean;
58
+ sms_enabled: boolean;
59
+ whatsapp_enabled: boolean;
60
+ push_enabled: boolean;
61
+ email_order_confirmations: boolean;
62
+ email_delivery_updates: boolean;
63
+ email_low_stock_alerts: boolean;
64
+ email_payment_received: boolean;
65
+ push_order_confirmations: boolean;
66
+ push_delivery_updates: boolean;
67
+ push_low_stock_alerts: boolean;
68
+ push_payment_received: boolean;
69
+ whatsapp_order_confirmations: boolean;
70
+ whatsapp_delivery_updates: boolean;
71
+ whatsapp_payment_received: boolean;
72
+ sms_order_confirmations: boolean;
73
+ sms_delivery_updates: boolean;
74
+ }
75
+
76
+
56
77
  // ── Business Context (storefront is scoped to one business) ──────────────────
57
78
 
58
79
  export interface BusinessContext {
@@ -90,8 +111,8 @@ export interface CustomerLoginPayload {
90
111
  /** POST /auth/register/customer/ */
91
112
  export interface CustomerRegisterPayload {
92
113
  tenant_id: string; // config.api.storefrontBusinessId
93
- /** Full name — backend splits this into firstname and lastname on the Profile */
94
- full_name: string;
114
+ first_name?: string;
115
+ last_name?: string;
95
116
  email: string;
96
117
  password: string;
97
118
  }
@@ -123,4 +144,6 @@ export interface AuthContextValue {
123
144
  closeModal: () => void;
124
145
  setCustomer: (user: CustomerUser | null) => void;
125
146
  logout: () => Promise<void>;
147
+ login: (payload: Omit<CustomerLoginPayload, 'tenant_id'>) => Promise<void>;
148
+ register: (payload: Omit<CustomerRegisterPayload, 'tenant_id'>) => Promise<{ requires_verification: boolean }>;
126
149
  }
@@ -1,6 +1,11 @@
1
1
  export interface BusinessConfig {
2
2
  business_id: string;
3
- store_name: string;
4
- store_currency: string;
3
+ storefront_business_id: string;
4
+ name: string;
5
+ currency: string;
6
+ settlement_currency: string;
7
+ country: string;
8
+ region: string;
9
+ is_verified: boolean;
5
10
  is_active: boolean;
6
11
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file types/payments/index.ts
3
- * @description All customer-facing payment types for the Luxe Payment System.
3
+ * @description All customer-facing payment types for the Kasuvia Payment System.
4
4
  *
5
5
  * Auth model:
6
6
  * - X-Storefront-Key → identifies the business storefront
@@ -9,8 +9,8 @@
9
9
  *
10
10
  * Payment flow:
11
11
  * Customer → Business Owner : Monnify (NGN) | Flutterwave (non-NGN)
12
- * Business Owner → Luxe : Subscription (separate, JWT-auth)
13
- * Luxe → Business Owner : Payout (separate, JWT-auth)
12
+ * Business Owner → Kasuvia : Subscription (separate, JWT-auth)
13
+ * Kasuvia → Business Owner : Payout (separate, JWT-auth)
14
14
  */
15
15
 
16
16
  // ── Configuration Types ─────────────────────────────────────────────────────────
@@ -2,9 +2,6 @@
2
2
  * Service booking types for customer-facing storefront
3
3
  */
4
4
 
5
- export type ServiceLocationType = "remote" | "physical" | "both";
6
- export type ServiceType = "appointment" | "project" | "recurring";
7
-
8
5
  export type AppointmentStatus =
9
6
  | "pending"
10
7
  | "confirmed"
@@ -29,7 +26,7 @@ export interface ServiceAppointment {
29
26
  appointment_date: string;
30
27
  start_time: string;
31
28
  end_time: string;
32
- location_type: ServiceLocationType;
29
+ location_type: import("../store").ServiceLocationType;
33
30
  customer_address: Record<string, unknown> | null;
34
31
  meeting_link: string | null;
35
32
  customer_notes: string | null;
@@ -45,7 +42,7 @@ export interface ServiceAppointmentCreatePayload {
45
42
  product_id: string;
46
43
  scheduled_date: string;
47
44
  scheduled_time: string;
48
- location_type?: ServiceLocationType;
45
+ location_type?: import("../store").ServiceLocationType;
49
46
  customer_address?: Record<string, unknown>;
50
47
  customer_notes?: string;
51
48
  }
@@ -1,8 +1,33 @@
1
- import { DeliveryAddress } from "./shipping";
1
+ import type { DeliveryAddress } from "./shipping";
2
+
3
+ export * from "./shipping";
4
+ export type { DeliveryAddress };
2
5
 
3
6
  export type ProductType = "physical" | "digital" | "service" | "bespoke";
4
7
 
5
- export type { DeliveryAddress } from "./shipping";
8
+ export enum ServiceTypeEnum {
9
+ APPOINTMENT = 'appointment',
10
+ PROJECT = 'project',
11
+ RECURRING = 'recurring',
12
+ }
13
+
14
+ export type ServiceType = ServiceTypeEnum | 'appointment' | 'project' | 'recurring'
15
+
16
+ export enum ServiceLocationTypeEnum {
17
+ REMOTE = 'remote',
18
+ PHYSICAL = 'physical',
19
+ BOTH = 'both',
20
+ }
21
+
22
+ export type ServiceLocationType = ServiceLocationTypeEnum | 'remote' | 'physical' | 'both'
23
+
24
+ export enum ServiceRecurringIntervalEnum {
25
+ WEEKLY = 'weekly',
26
+ MONTHLY = 'monthly',
27
+ YEARLY = 'yearly',
28
+ }
29
+
30
+ export type ServiceRecurringInterval = ServiceRecurringIntervalEnum | 'weekly' | 'monthly' | 'yearly' | ''
6
31
 
7
32
  export interface Address {
8
33
  street: string
@@ -18,7 +43,7 @@ export interface Collection {
18
43
  name: string
19
44
  slug: string
20
45
  description?: string
21
- image?: { url: string; alt?: string; id?: string }
46
+ image?: string
22
47
  collection_type?: string
23
48
  rules?: Record<string, unknown>
24
49
  status?: string
@@ -49,7 +74,7 @@ export interface ProductVariant {
49
74
  sku?: string
50
75
  attributes: Record<string, string>
51
76
  price?: number
52
- images: Array<{ url: string; alt?: string; id?: string }>
77
+ images: string[]
53
78
  weight_kg?: number
54
79
  is_active: boolean
55
80
  inventory?: Inventory
@@ -77,9 +102,13 @@ export interface Product {
77
102
  requires_shipping: boolean
78
103
  is_consumable: boolean
79
104
  is_bespoke: boolean
105
+ is_physical?: boolean | null // For bespoke products: indicates if custom item requires physical delivery
80
106
  base_price: number
81
107
  currency: string
82
- images: Array<{ url: string; alt?: string; id?: string }>
108
+ sale_price?: number
109
+ sale_start?: string
110
+ sale_end?: string
111
+ images: string[]
83
112
  tags: string[]
84
113
  custom_fields: CustomField[]
85
114
  is_published: boolean
@@ -90,6 +119,16 @@ export interface Product {
90
119
  digital_file_key?: string
91
120
  download_limit?: number
92
121
  download_expires_at?: string
122
+ // Service-specific fields
123
+ service_type?: ServiceType | null
124
+ service_duration_minutes?: number | null
125
+ service_location_type?: ServiceLocationType | null
126
+ service_requires_consultation?: boolean
127
+ service_max_capacity_per_slot?: number | null
128
+ service_buffer_minutes?: number
129
+ service_is_recurring?: boolean
130
+ service_recurring_interval?: ServiceRecurringInterval | null
131
+ service_project_based?: boolean
93
132
  weight_kg: number
94
133
  length_cm?: number
95
134
  width_cm?: number
@@ -18,3 +18,17 @@ export function getCustomerDisplayName(user: CustomerUser): string {
18
18
  const fromParts = buildFullName(user.firstname, user.lastname)
19
19
  return fromParts || user.full_name || user.email.split('@')[0]
20
20
  }
21
+
22
+ /**
23
+ * Constructs live storefront URL for a business.
24
+ * If custom domain is not yet connected/verified, defaults to `https://<business_id>.kasuvia.com`.
25
+ */
26
+ export function getStorefrontUrl(businessId: string, customDomain?: string | null): string {
27
+ if (customDomain && customDomain.trim()) {
28
+ const cleanDomain = customDomain.trim().replace(/^https?:\/\//, '');
29
+ return `https://${cleanDomain}`;
30
+ }
31
+ const cleanBid = (businessId || 'store').toLowerCase().trim();
32
+ return `https://${cleanBid}.kasuvia.com`;
33
+ }
34
+
@@ -1,14 +1,19 @@
1
- import axios from 'axios';
2
-
3
1
  /**
4
2
  * Type-safe way to extract an error message from an API error.
3
+ * Updated for fetch-based errors (no axios dependency).
5
4
  */
6
5
  export function getErrorMessage(error: unknown, fallback: string = "Something went wrong"): string {
7
- if (axios.isAxiosError(error)) {
8
- return error.response?.data?.detail || error.response?.data?.message || error.message || fallback;
9
- }
10
6
  if (error instanceof Error) {
11
7
  return error.message;
12
8
  }
9
+ // Handle fetch Response objects that might be thrown
10
+ if (error instanceof Response) {
11
+ return `HTTP ${error.status}: ${error.statusText}`;
12
+ }
13
+ // Handle objects with detail or message properties (from API responses)
14
+ if (typeof error === 'object' && error !== null) {
15
+ const err = error as { detail?: string; message?: string; status?: number };
16
+ return err.detail || err.message || fallback;
17
+ }
13
18
  return fallback;
14
19
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @file storage.ts
3
+ * @description Centralized tenant-isolated storage utility for sessionStorage and localStorage.
4
+ * Automatically prefixes all keys with the storefront Business ID to guarantee multi-tenant segregation.
5
+ * Stateless - requires businessId parameter.
6
+ */
7
+
8
+ function getPrefixedKey(businessId: string, key: string): string {
9
+ const cleanKey = key.startsWith('kasuvia_') ? key.substring(5) : key
10
+ return businessId ? `kasuvia-sf-${businessId}-${cleanKey}` : `kasuvia-sf-anon-${cleanKey}`
11
+ }
12
+
13
+ export const sessionStore = {
14
+ get: (businessIdOrKey: string, key?: string): string | null => {
15
+ if (typeof window === 'undefined') return null
16
+ const bId = key ? businessIdOrKey : ''
17
+ const k = key || businessIdOrKey
18
+ return sessionStorage.getItem(getPrefixedKey(bId, k))
19
+ },
20
+ set: (businessIdOrKey: string, keyOrVal: string, val?: string): void => {
21
+ if (typeof window === 'undefined') return
22
+ const bId = val !== undefined ? businessIdOrKey : ''
23
+ const k = val !== undefined ? keyOrVal : businessIdOrKey
24
+ const v = val !== undefined ? val : keyOrVal
25
+ sessionStorage.setItem(getPrefixedKey(bId, k), v)
26
+ },
27
+ remove: (businessIdOrKey: string, key?: string): void => {
28
+ if (typeof window === 'undefined') return
29
+ const bId = key ? businessIdOrKey : ''
30
+ const k = key || businessIdOrKey
31
+ sessionStorage.removeItem(getPrefixedKey(bId, k))
32
+ },
33
+ getJSON: <T = unknown>(businessIdOrKey: string, key?: string): T | null => {
34
+ if (typeof window === 'undefined') return null
35
+ const bId = key ? businessIdOrKey : ''
36
+ const k = key || businessIdOrKey
37
+ const value = sessionStorage.getItem(getPrefixedKey(bId, k))
38
+ if (!value) return null
39
+ try {
40
+ return JSON.parse(value) as T
41
+ } catch {
42
+ return null
43
+ }
44
+ },
45
+ setJSON: (businessIdOrKey: string, keyOrVal: unknown, val?: unknown): void => {
46
+ if (typeof window === 'undefined') return
47
+ const bId = val !== undefined ? (businessIdOrKey as string) : ''
48
+ const k = val !== undefined ? (keyOrVal as string) : (businessIdOrKey as string)
49
+ const v = val !== undefined ? val : keyOrVal
50
+ sessionStorage.setItem(getPrefixedKey(bId, k), JSON.stringify(v))
51
+ },
52
+ }
53
+
54
+ export const localStore = {
55
+ get: (businessIdOrKey: string, key?: string): string | null => {
56
+ if (typeof window === 'undefined') return null
57
+ const bId = key ? businessIdOrKey : ''
58
+ const k = key || businessIdOrKey
59
+ return localStorage.getItem(getPrefixedKey(bId, k))
60
+ },
61
+ set: (businessIdOrKey: string, keyOrVal: string, val?: string): void => {
62
+ if (typeof window === 'undefined') return
63
+ const bId = val !== undefined ? businessIdOrKey : ''
64
+ const k = val !== undefined ? keyOrVal : businessIdOrKey
65
+ const v = val !== undefined ? val : keyOrVal
66
+ localStorage.setItem(getPrefixedKey(bId, k), v)
67
+ },
68
+ remove: (businessIdOrKey: string, key?: string): void => {
69
+ if (typeof window === 'undefined') return
70
+ const bId = key ? businessIdOrKey : ''
71
+ const k = key || businessIdOrKey
72
+ localStorage.removeItem(getPrefixedKey(bId, k))
73
+ },
74
+ getJSON: <T = unknown>(businessIdOrKey: string, key?: string): T | null => {
75
+ if (typeof window === 'undefined') return null
76
+ const bId = key ? businessIdOrKey : ''
77
+ const k = key || businessIdOrKey
78
+ const value = localStorage.getItem(getPrefixedKey(bId, k))
79
+ if (!value) return null
80
+ try {
81
+ return JSON.parse(value) as T
82
+ } catch {
83
+ return null
84
+ }
85
+ },
86
+ setJSON: (businessIdOrKey: string, keyOrVal: unknown, val?: unknown): void => {
87
+ if (typeof window === 'undefined') return
88
+ const bId = val !== undefined ? (businessIdOrKey as string) : ''
89
+ const k = val !== undefined ? (keyOrVal as string) : (businessIdOrKey as string)
90
+ const v = val !== undefined ? val : keyOrVal
91
+ localStorage.setItem(getPrefixedKey(bId, k), JSON.stringify(v))
92
+ },
93
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Form validation utilities for kasuvia-sdk
3
+ * Provides realtime validation for email, password, name, confirm password, and message fields
4
+ */
5
+
6
+ export function validateEmail(email: string): string | null {
7
+ if (!email || email.trim() === "") {
8
+ return "Email is required";
9
+ }
10
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
11
+ if (!emailRegex.test(email)) {
12
+ return "Please enter a valid email address";
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export function validatePassword(password: string): string | null {
18
+ if (!password || password.trim() === "") {
19
+ return "Password is required";
20
+ }
21
+ if (password.length < 8) {
22
+ return "Password must be at least 8 characters";
23
+ }
24
+ if (password.length > 72) {
25
+ return "Password cannot exceed 72 characters";
26
+ }
27
+ if (!/[A-Z]/.test(password)) {
28
+ return "Password must contain at least one uppercase letter";
29
+ }
30
+ if (!/[a-z]/.test(password)) {
31
+ return "Password must contain at least one lowercase letter";
32
+ }
33
+ if (!/[0-9]/.test(password)) {
34
+ return "Password must contain at least one number";
35
+ }
36
+ return null;
37
+ }
38
+
39
+ export function validateName(name: string, fieldName: string = "Name"): string | null {
40
+ if (!name || name.trim() === "") {
41
+ return `${fieldName} is required`;
42
+ }
43
+ if (name.trim().length < 2) {
44
+ return `${fieldName} must be at least 2 characters`;
45
+ }
46
+ if (name.trim().length > 50) {
47
+ return `${fieldName} cannot exceed 50 characters`;
48
+ }
49
+ // Allow letters, spaces, hyphens, and apostrophes
50
+ const nameRegex = /^[a-zA-Z\s\-'\u00C0-\u017F]+$/;
51
+ if (!nameRegex.test(name.trim())) {
52
+ return `${fieldName} can only contain letters, spaces, hyphens, and apostrophes`;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export function validateRequired(value: string, fieldName: string): string | null {
58
+ if (!value || value.trim() === "") {
59
+ return `${fieldName} is required`;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Validates the confirm-password field.
66
+ * Returns null (no error) when the field is empty — avoids nagging the user
67
+ * before they've had a chance to type anything.
68
+ */
69
+ export function validateConfirmPassword(password: string, confirmPassword: string): string | null {
70
+ if (!confirmPassword) return null;
71
+ if (confirmPassword !== password) {
72
+ return "Passwords do not match";
73
+ }
74
+ return null;
75
+ }
76
+
77
+ export function validateMessage(message: string): string | null {
78
+ if (!message || message.trim() === "") {
79
+ return "Message is required";
80
+ }
81
+ if (message.trim().length < 10) {
82
+ return "Message must be at least 10 characters";
83
+ }
84
+ return null;
85
+ }