@kasuvia/sdk 0.0.7 → 0.0.9
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/README.md +42 -15
- package/dist/AddressAutocomplete-CjcqPElD.d.mts +46 -0
- package/dist/AddressAutocomplete-CvJHuRkf.d.ts +46 -0
- package/dist/components/index.d.mts +497 -0
- package/dist/components/index.d.ts +497 -0
- package/dist/components/index.js +4217 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +4194 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/server.d.mts +26 -0
- package/dist/config/server.d.ts +26 -0
- package/dist/config/server.js +38 -0
- package/dist/config/server.js.map +1 -0
- package/dist/config/server.mjs +13 -0
- package/dist/config/server.mjs.map +1 -0
- package/dist/hooks/index.d.mts +393 -160
- package/dist/hooks/index.d.ts +393 -160
- package/dist/hooks/index.js +2792 -2137
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +2734 -2121
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index-CNW_LINy.d.mts +578 -0
- package/dist/index-CNW_LINy.d.ts +578 -0
- package/dist/{index-C1chUA3P.d.mts → index-F7N6UItS.d.mts} +1 -1
- package/dist/{index-C1chUA3P.d.ts → index-F7N6UItS.d.ts} +1 -1
- package/dist/index-NkI10J_E.d.mts +40 -0
- package/dist/index-NkI10J_E.d.ts +40 -0
- package/dist/index.d.mts +20437 -103
- package/dist/index.d.ts +20437 -103
- package/dist/index.js +2111 -1802
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2006 -1786
- package/dist/index.mjs.map +1 -1
- package/dist/media.service-Cx7dQjzX.d.mts +1393 -0
- package/dist/media.service-mrBFJ2UQ.d.ts +1393 -0
- package/dist/services/index.d.mts +908 -2
- package/dist/services/index.d.ts +908 -2
- package/dist/services/index.js +1492 -1686
- package/dist/services/index.js.map +1 -1
- package/dist/services/index.mjs +1431 -1670
- package/dist/services/index.mjs.map +1 -1
- package/package.json +46 -6
- package/src/components/bespoke/BespokeAvailabilityBadge.tsx +32 -0
- package/src/components/bespoke/BespokeProgressTracker.tsx +58 -0
- package/src/components/bespoke/index.ts +3 -0
- package/src/components/booking/SlotPicker.tsx +97 -0
- package/src/components/booking/index.ts +1 -0
- package/src/components/cart/CartItem.tsx +81 -0
- package/src/components/cart/CartSidebar.tsx +115 -0
- package/src/components/cart/CartTypeMismatchBanner.tsx +55 -0
- package/src/components/cart/index.ts +3 -0
- package/src/components/index.ts +8 -0
- package/src/components/orders/DeliverableReviewer.tsx +139 -0
- package/src/components/orders/index.ts +1 -0
- package/src/components/product/ProductCard.tsx +350 -0
- package/src/components/product/ProductGrid.tsx +84 -0
- package/src/components/product/index.ts +2 -0
- package/src/components/shared/AddressAutocomplete.tsx +212 -0
- package/src/components/shared/AnalyticsScripts.tsx +56 -0
- package/src/components/shared/CookieConsentBanner.tsx +98 -0
- package/src/components/shared/ImageEditorModal.tsx +1066 -0
- package/src/components/shared/ImageViewerModal.tsx +598 -0
- package/src/components/shared/LucideReactIcon.tsx +52 -0
- package/src/components/shared/MapPinDrop.tsx +101 -0
- package/src/components/shared/MultiSelectPicker.tsx +193 -0
- package/src/components/shared/OfflineBanner.tsx +46 -0
- package/src/components/shared/SearchableInput.tsx +153 -0
- package/src/components/shared/SearchablePopover.tsx +166 -0
- package/src/components/shared/SmartImage.tsx +311 -0
- package/src/components/shared/TruncatedValue.tsx +27 -0
- package/src/components/shared/UnsavedChangesGuard.tsx +138 -0
- package/src/components/shared/index.ts +15 -0
- package/src/components/skeletons/CardSkeleton.tsx +26 -0
- package/src/components/skeletons/CartItemSkeleton.tsx +26 -0
- package/src/components/skeletons/OrderItemSkeleton.tsx +26 -0
- package/src/components/skeletons/ProductCardSkeleton.tsx +26 -0
- package/src/components/skeletons/TableSkeleton.tsx +42 -0
- package/src/components/skeletons/index.ts +5 -0
- package/src/config/index.ts +22 -4
- package/src/config/server.ts +45 -0
- package/src/context/sdk-context.tsx +5 -2
- package/src/generated/auth-openapi.ts +2935 -0
- package/src/generated/cart-openapi.ts +652 -0
- package/src/generated/cms-openapi.ts +1668 -0
- package/src/generated/crm-openapi.ts +986 -0
- package/src/generated/graphql.ts +126 -0
- package/src/generated/index.ts +11 -0
- package/src/generated/media-openapi.ts +613 -0
- package/src/generated/notification-openapi.ts +862 -0
- package/src/generated/payment-openapi.ts +2713 -0
- package/src/generated/store-openapi.ts +9510 -0
- package/src/graphql/operations/collections.graphql +32 -0
- package/src/graphql/operations/products.graphql +197 -0
- package/src/graphql/operations/wishlist.graphql +25 -0
- package/src/hooks/index.ts +9 -2
- package/src/hooks/use-auth.ts +163 -69
- package/src/hooks/use-bespoke-availability.ts +39 -22
- package/src/hooks/use-business-config.ts +51 -0
- package/src/hooks/use-cart.ts +310 -238
- package/src/hooks/use-cms.ts +107 -127
- package/src/hooks/use-collections.ts +35 -55
- package/src/hooks/use-consultations.ts +9 -136
- package/src/hooks/use-countries.ts +19 -28
- package/src/hooks/use-currencies.ts +8 -20
- package/src/hooks/use-customer-address.ts +64 -0
- package/src/hooks/use-feedback.ts +34 -0
- package/src/hooks/use-languages.ts +26 -39
- package/src/hooks/use-media-upload.ts +23 -0
- package/src/hooks/use-notification-stream.ts +88 -0
- package/src/hooks/use-notifications.ts +163 -0
- package/src/hooks/use-order-stream.ts +29 -29
- package/src/hooks/use-orders.ts +91 -73
- package/src/hooks/use-payment.ts +116 -84
- package/src/hooks/use-products.ts +65 -269
- package/src/hooks/use-push-notifications.ts +122 -0
- package/src/hooks/use-reviews.ts +175 -0
- package/src/hooks/use-service-booking.ts +109 -132
- package/src/hooks/use-states.ts +16 -30
- package/src/hooks/use-wishlist.ts +44 -69
- package/src/index.ts +15 -11
- package/src/services/auth.service.ts +422 -132
- package/src/services/bespoke.service.ts +69 -10
- package/src/services/cart.service.ts +155 -186
- package/src/services/cms.service.ts +41 -50
- package/src/services/collection.service.ts +25 -63
- package/src/services/config.service.ts +10 -21
- package/src/services/consultation.service.ts +36 -93
- package/src/services/feedback.service.ts +76 -0
- package/src/services/index.ts +7 -1
- package/src/services/legal.service.ts +62 -0
- package/src/services/logistics.service.ts +239 -54
- package/src/services/media.service.ts +115 -0
- package/src/services/notifications.service.ts +204 -20
- package/src/services/order.service.ts +498 -127
- package/src/services/payment.service.ts +149 -100
- package/src/services/product.service.ts +157 -814
- package/src/services/review.service.ts +211 -0
- package/src/services/service-booking.service.ts +198 -99
- package/src/services/wishlist.service.ts +27 -74
- package/src/types/auth/index.ts +161 -18
- package/src/types/business/index.ts +9 -1
- package/src/types/cms/index.ts +91 -6
- package/src/types/cms/legal.ts +18 -0
- package/src/types/consultations/index.ts +60 -68
- package/src/types/index.ts +5 -3
- package/src/types/media/index.ts +35 -3
- package/src/types/payments/index.ts +80 -16
- package/src/types/reviews/index.ts +116 -0
- package/src/types/services/index.ts +113 -38
- package/src/types/store/checkout.ts +53 -0
- package/src/types/store/index.ts +273 -52
- package/src/types/store/invoice.ts +31 -0
- package/src/types/store/shipping.ts +95 -3
- package/src/types/ui/index.ts +1 -1
- package/src/utils/checkout.ts +19 -3
- package/src/utils/cookieConsent.ts +34 -0
- package/src/utils/format.ts +408 -0
- package/src/utils/geolocation.ts +134 -0
- package/src/utils/graphql-url.ts +47 -0
- package/src/utils/guest-session.ts +59 -0
- package/src/utils/http-client.ts +147 -0
- package/src/utils/index.ts +11 -4
- package/src/utils/status-colors.ts +79 -0
- package/src/utils/token-refresh.ts +115 -0
- package/src/utils/validators.ts +13 -0
- package/src/utils/youtube.ts +18 -0
- package/dist/index-D9ojrziq.d.mts +0 -786
- package/dist/index-ac_Jx9NI.d.mts +0 -492
- package/dist/index-ac_Jx9NI.d.ts +0 -492
- package/dist/index-hgK4j47a.d.ts +0 -786
- package/src/graphql/collections.graphql.ts +0 -48
- package/src/graphql/index.ts +0 -7
- package/src/graphql/products.graphql.ts +0 -860
- package/src/graphql/wishlist.graphql.ts +0 -34
- package/src/hooks/use-consultation-stream.ts +0 -142
- package/src/types/bespoke/index.ts +0 -39
|
@@ -4,75 +4,88 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { SdkConfig } from '../config'
|
|
6
6
|
import { STORAGE_KEYS } from '../constants'
|
|
7
|
+
import { apiRequest, ApiError, storefrontHeaders, authHeaders, JSON_HEADERS } from '../utils/http-client'
|
|
8
|
+
import { buildFullName, splitFullName } from '../utils/customer'
|
|
7
9
|
import type {
|
|
8
10
|
CustomerAuthResponse,
|
|
9
11
|
CustomerLoginPayload,
|
|
10
12
|
CustomerRegisterPayload,
|
|
13
|
+
CustomerPasswordlessRequestPayload,
|
|
14
|
+
CustomerPasswordlessVerifyPayload,
|
|
15
|
+
CustomerForgotPasswordPayload,
|
|
16
|
+
CustomerResetPasswordPayload,
|
|
17
|
+
CustomerDeactivatePayload,
|
|
18
|
+
DeactivateAccountResult,
|
|
19
|
+
DeactivateConfirmationRequired,
|
|
20
|
+
CustomerReactivatePayload,
|
|
21
|
+
CustomerReactivateVerifyPayload,
|
|
11
22
|
LogoutPayload,
|
|
12
23
|
RefreshTokenResponse,
|
|
13
24
|
CustomerUser,
|
|
14
25
|
AuthUserResponse,
|
|
15
26
|
UpdateProfilePayload,
|
|
27
|
+
StorefrontBusinessInfo,
|
|
28
|
+
ChangePasswordPayload,
|
|
29
|
+
CustomerAddress,
|
|
30
|
+
CreateCustomerAddressPayload,
|
|
31
|
+
UpdateCustomerAddressPayload,
|
|
16
32
|
} from '../types/auth'
|
|
17
33
|
|
|
34
|
+
import type { MediaAsset } from '../types/media'
|
|
35
|
+
|
|
18
36
|
// ── Auth API Calls ────────────────────────────────────────────────────────────
|
|
19
37
|
|
|
20
38
|
/**
|
|
21
39
|
* POST /auth/login/customer/
|
|
22
|
-
*
|
|
40
|
+
* Business is identified by the X-Storefront-Key + X-Business-ID headers
|
|
41
|
+
* (config.storefrontApiKey / config.businessId) — never a body field (see
|
|
42
|
+
* plan: Customer Storefront Identity Enforcement).
|
|
23
43
|
*/
|
|
24
44
|
export async function loginCustomer(
|
|
25
45
|
config: SdkConfig,
|
|
26
|
-
payload:
|
|
46
|
+
payload: CustomerLoginPayload,
|
|
47
|
+
guestSessionId?: string | null
|
|
27
48
|
): Promise<CustomerAuthResponse> {
|
|
28
|
-
const
|
|
49
|
+
const headers: Record<string, string> = { ...JSON_HEADERS, ...storefrontHeaders(config) }
|
|
50
|
+
// REAL FEATURE FOUND (2026-09-21): kasuvia-auth's login/register/OTP-verify
|
|
51
|
+
// views already run a real server-side guest-cart merge the moment tokens
|
|
52
|
+
// are minted (customer.py's _run_post_login_merge, reading this exact
|
|
53
|
+
// header) — deliberately built to replace depending on a frontend
|
|
54
|
+
// remembering to call kasuvia-cart-management's /cart/merge afterward (see
|
|
55
|
+
// cart_merge_client.py's own doc comment). This was simply never wired:
|
|
56
|
+
// nothing in the storefront ever sent this header on login.
|
|
57
|
+
if (guestSessionId) {
|
|
58
|
+
headers['X-Guest-Session'] = guestSessionId
|
|
59
|
+
}
|
|
60
|
+
return apiRequest<CustomerAuthResponse>(`${config.backendUrls.auth}/auth/login/customer/`, {
|
|
29
61
|
method: 'POST',
|
|
30
|
-
headers
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'X-Business-ID': config.businessId,
|
|
34
|
-
},
|
|
35
|
-
body: JSON.stringify({
|
|
36
|
-
...payload,
|
|
37
|
-
tenant_id: config.businessId,
|
|
38
|
-
}),
|
|
62
|
+
headers,
|
|
63
|
+
body: JSON.stringify(payload),
|
|
64
|
+
fallbackErrorMessage: 'Login failed',
|
|
39
65
|
})
|
|
40
|
-
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
throw new Error('Login failed')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const data = await response.json()
|
|
46
|
-
return data
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
/**
|
|
50
69
|
* POST /auth/register/customer/
|
|
51
|
-
*
|
|
70
|
+
* Business identified via headers, see loginCustomer.
|
|
52
71
|
*/
|
|
53
72
|
export async function registerCustomer(
|
|
54
73
|
config: SdkConfig,
|
|
55
|
-
payload:
|
|
74
|
+
payload: CustomerRegisterPayload,
|
|
75
|
+
guestSessionId?: string | null
|
|
56
76
|
): Promise<CustomerAuthResponse> {
|
|
57
|
-
const
|
|
77
|
+
const headers: Record<string, string> = { ...JSON_HEADERS, ...storefrontHeaders(config) }
|
|
78
|
+
// See loginCustomer's identical comment — same real, already-built
|
|
79
|
+
// server-side merge, same header.
|
|
80
|
+
if (guestSessionId) {
|
|
81
|
+
headers['X-Guest-Session'] = guestSessionId
|
|
82
|
+
}
|
|
83
|
+
return apiRequest<CustomerAuthResponse>(`${config.backendUrls.auth}/auth/register/customer/`, {
|
|
58
84
|
method: 'POST',
|
|
59
|
-
headers
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
'X-Business-ID': config.businessId,
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify({
|
|
65
|
-
...payload,
|
|
66
|
-
tenant_id: config.businessId,
|
|
67
|
-
}),
|
|
85
|
+
headers,
|
|
86
|
+
body: JSON.stringify(payload),
|
|
87
|
+
fallbackErrorMessage: 'Registration failed',
|
|
68
88
|
})
|
|
69
|
-
|
|
70
|
-
if (!response.ok) {
|
|
71
|
-
throw new Error('Registration failed')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const data = await response.json()
|
|
75
|
-
return data
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
/** POST /auth/token/refresh/
|
|
@@ -81,38 +94,44 @@ export async function refreshToken(
|
|
|
81
94
|
config: SdkConfig,
|
|
82
95
|
refresh: string
|
|
83
96
|
): Promise<RefreshTokenResponse> {
|
|
84
|
-
|
|
97
|
+
return apiRequest<RefreshTokenResponse>(`${config.backendUrls.auth}/auth/token/refresh/`, {
|
|
85
98
|
method: 'POST',
|
|
86
|
-
headers:
|
|
87
|
-
'Content-Type': 'application/json',
|
|
88
|
-
},
|
|
99
|
+
headers: JSON_HEADERS,
|
|
89
100
|
body: JSON.stringify({ refresh }),
|
|
101
|
+
fallbackErrorMessage: 'Token refresh failed',
|
|
90
102
|
})
|
|
91
|
-
|
|
92
|
-
if (!response.ok) {
|
|
93
|
-
throw new Error('Token refresh failed')
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const data = await response.json()
|
|
97
|
-
return data
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* POST /auth/logout/ — revokes the server-side session.
|
|
107
|
+
*
|
|
108
|
+
* Never throws: a customer's own logout must always clear their local
|
|
109
|
+
* session client-side (caller does this regardless of this promise's
|
|
110
|
+
* outcome) even if the backend is unreachable — an unrevoked server-side
|
|
111
|
+
* session in that case is a availability tradeoff, not a security one (the
|
|
112
|
+
* refresh token still expires on its own TTL). What CLAUDE.md forbids is
|
|
113
|
+
* failing SILENTLY; the failure is now always logged with real context so
|
|
114
|
+
* it's visible to whoever is debugging a report of "sessions not clearing
|
|
115
|
+
* server-side", instead of vanishing into a bare `catch {}`.
|
|
116
|
+
*/
|
|
101
117
|
export async function logout(
|
|
102
118
|
config: SdkConfig,
|
|
103
119
|
refresh?: string
|
|
104
120
|
): Promise<void> {
|
|
105
121
|
const payload: LogoutPayload = refresh ? { refresh } : {}
|
|
106
122
|
try {
|
|
107
|
-
await
|
|
123
|
+
await apiRequest(`${config.backendUrls.auth}/auth/logout/`, {
|
|
108
124
|
method: 'POST',
|
|
109
|
-
headers:
|
|
110
|
-
'Content-Type': 'application/json',
|
|
111
|
-
},
|
|
125
|
+
headers: JSON_HEADERS,
|
|
112
126
|
body: JSON.stringify(payload),
|
|
127
|
+
fallbackErrorMessage: 'Server-side logout failed',
|
|
113
128
|
})
|
|
114
|
-
} catch {
|
|
115
|
-
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err instanceof ApiError) {
|
|
131
|
+
console.error(`[auth.service] Server-side logout failed with status ${err.status}; local session will still be cleared.`)
|
|
132
|
+
} else {
|
|
133
|
+
console.error('[auth.service] Server-side logout request failed; local session will still be cleared.', err)
|
|
134
|
+
}
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
|
|
@@ -124,25 +143,154 @@ export async function verifySocialToken(
|
|
|
124
143
|
provider: 'google' | 'facebook',
|
|
125
144
|
token: string
|
|
126
145
|
): Promise<CustomerAuthResponse> {
|
|
127
|
-
|
|
146
|
+
return apiRequest<CustomerAuthResponse>(`${config.backendUrls.auth}/auth/social/verify/${provider}/`, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
|
|
149
|
+
body: JSON.stringify({ id_token: token }),
|
|
150
|
+
fallbackErrorMessage: 'Social token verification failed',
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* POST /auth/login/customer/passwordless/request/
|
|
156
|
+
* Sends a one-time login code to the customer's email. Always resolves
|
|
157
|
+
* successfully (the backend never reveals whether the email is registered).
|
|
158
|
+
* Business identified via headers, see loginCustomer.
|
|
159
|
+
*/
|
|
160
|
+
export async function requestCustomerOtp(
|
|
161
|
+
config: SdkConfig,
|
|
162
|
+
payload: CustomerPasswordlessRequestPayload
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
await apiRequest(`${config.backendUrls.auth}/auth/login/customer/passwordless/request/`, {
|
|
128
165
|
method: 'POST',
|
|
129
|
-
headers: {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
body: JSON.stringify({
|
|
133
|
-
id_token: token,
|
|
134
|
-
storefront_key: config.storefrontApiKey,
|
|
135
|
-
}),
|
|
166
|
+
headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
|
|
167
|
+
body: JSON.stringify(payload),
|
|
168
|
+
fallbackErrorMessage: 'Failed to send login code',
|
|
136
169
|
})
|
|
170
|
+
}
|
|
137
171
|
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
/**
|
|
173
|
+
* POST /auth/login/customer/passwordless/verify/
|
|
174
|
+
* Verifies a one-time login code and returns tokens, auto-creating the
|
|
175
|
+
* customer account on first use (same as social login).
|
|
176
|
+
* Business identified via headers, see loginCustomer.
|
|
177
|
+
*/
|
|
178
|
+
export async function verifyCustomerOtp(
|
|
179
|
+
config: SdkConfig,
|
|
180
|
+
payload: CustomerPasswordlessVerifyPayload,
|
|
181
|
+
guestSessionId?: string | null
|
|
182
|
+
): Promise<CustomerAuthResponse> {
|
|
183
|
+
const headers: Record<string, string> = { ...JSON_HEADERS, ...storefrontHeaders(config) }
|
|
184
|
+
// See loginCustomer's identical comment — same real, already-built
|
|
185
|
+
// server-side merge, same header.
|
|
186
|
+
if (guestSessionId) {
|
|
187
|
+
headers['X-Guest-Session'] = guestSessionId
|
|
140
188
|
}
|
|
189
|
+
return apiRequest<CustomerAuthResponse>(`${config.backendUrls.auth}/auth/login/customer/passwordless/verify/`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers,
|
|
192
|
+
body: JSON.stringify(payload),
|
|
193
|
+
fallbackErrorMessage: 'Invalid or expired code',
|
|
194
|
+
})
|
|
195
|
+
}
|
|
141
196
|
|
|
142
|
-
|
|
143
|
-
|
|
197
|
+
/**
|
|
198
|
+
* POST /auth/forgot-password/customer/
|
|
199
|
+
* Request a customer password reset OTP (business-scoped via headers).
|
|
200
|
+
*/
|
|
201
|
+
export async function forgotPasswordCustomer(
|
|
202
|
+
config: SdkConfig,
|
|
203
|
+
payload: CustomerForgotPasswordPayload
|
|
204
|
+
): Promise<{ detail: string }> {
|
|
205
|
+
return apiRequest<{ detail: string }>(`${config.backendUrls.auth}/auth/forgot-password/customer/`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
|
|
208
|
+
body: JSON.stringify(payload),
|
|
209
|
+
fallbackErrorMessage: 'Failed to request password reset',
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* POST /auth/reset-password/customer/
|
|
215
|
+
* Verify OTP and reset customer password (business-scoped via headers).
|
|
216
|
+
*/
|
|
217
|
+
export async function resetPasswordCustomer(
|
|
218
|
+
config: SdkConfig,
|
|
219
|
+
payload: CustomerResetPasswordPayload
|
|
220
|
+
): Promise<{ detail: string }> {
|
|
221
|
+
return apiRequest<{ detail: string }>(`${config.backendUrls.auth}/auth/reset-password/customer/`, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
|
|
224
|
+
body: JSON.stringify(payload),
|
|
225
|
+
fallbackErrorMessage: 'Invalid or expired reset code',
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* POST /auth/customer/deactivate/
|
|
231
|
+
* Self-service account deactivation for customers.
|
|
232
|
+
*/
|
|
233
|
+
export async function deactivateCustomerAccount(
|
|
234
|
+
config: SdkConfig,
|
|
235
|
+
accessToken: string,
|
|
236
|
+
payload?: CustomerDeactivatePayload
|
|
237
|
+
): Promise<DeactivateAccountResult> {
|
|
238
|
+
try {
|
|
239
|
+
const data = await apiRequest<{ detail: string }>(`${config.backendUrls.auth}/auth/customer/deactivate/`, {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
242
|
+
body: JSON.stringify(payload || {}),
|
|
243
|
+
fallbackErrorMessage: 'Failed to deactivate account',
|
|
244
|
+
})
|
|
245
|
+
return { status: 'success', detail: data.detail }
|
|
246
|
+
} catch (err) {
|
|
247
|
+
// REAL BUG FIXED (2026-09-20): a 409 here is NOT a failure — it's step 1
|
|
248
|
+
// of require_destructive_confirmation's real two-step contract (see
|
|
249
|
+
// DeactivateConfirmationRequired's doc comment). The FIRST call with no
|
|
250
|
+
// confirmation_code ALWAYS 409s by design, so this function as written
|
|
251
|
+
// before could never actually succeed — it threw a generic Error on every
|
|
252
|
+
// real call, discarding the impact list and confirmation_code the caller
|
|
253
|
+
// needs to complete the flow.
|
|
254
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
255
|
+
return { status: 'confirmation_required', data: err.body as DeactivateConfirmationRequired }
|
|
256
|
+
}
|
|
257
|
+
throw err
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* POST /auth/customer/reactivate/
|
|
263
|
+
* Request reactivation code for a deactivated customer account.
|
|
264
|
+
*/
|
|
265
|
+
export async function requestCustomerReactivation(
|
|
266
|
+
config: SdkConfig,
|
|
267
|
+
payload: CustomerReactivatePayload
|
|
268
|
+
): Promise<{ detail: string }> {
|
|
269
|
+
return apiRequest<{ detail: string }>(`${config.backendUrls.auth}/auth/customer/reactivate/`, {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
|
|
272
|
+
body: JSON.stringify(payload),
|
|
273
|
+
fallbackErrorMessage: 'Failed to request reactivation',
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* POST /auth/customer/reactivate/verify/
|
|
279
|
+
* Verify reactivation OTP and restore customer account.
|
|
280
|
+
*/
|
|
281
|
+
export async function verifyCustomerReactivation(
|
|
282
|
+
config: SdkConfig,
|
|
283
|
+
payload: CustomerReactivateVerifyPayload
|
|
284
|
+
): Promise<{ detail: string }> {
|
|
285
|
+
return apiRequest<{ detail: string }>(`${config.backendUrls.auth}/auth/customer/reactivate/verify/`, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: { ...JSON_HEADERS, ...storefrontHeaders(config) },
|
|
288
|
+
body: JSON.stringify(payload),
|
|
289
|
+
fallbackErrorMessage: 'Invalid or expired reactivation code',
|
|
290
|
+
})
|
|
144
291
|
}
|
|
145
292
|
|
|
293
|
+
|
|
146
294
|
/** Decode a JWT payload without verifying signature (client-side use only). */
|
|
147
295
|
function parseJwtPayload(token: string): Record<string, unknown> {
|
|
148
296
|
try {
|
|
@@ -211,47 +359,38 @@ function buildCustomerFromClaims(claims: Record<string, unknown>): CustomerUser
|
|
|
211
359
|
/** GET /auth/profile/ */
|
|
212
360
|
export async function getMe(config?: SdkConfig, accessToken?: string): Promise<CustomerUser> {
|
|
213
361
|
if (!config || !accessToken) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
362
|
+
// `/api/auth/me` proxies the raw AuthUserResponse shape (nested
|
|
363
|
+
// `.profile.firstname` etc) under `data.user` — it must be mapped the
|
|
364
|
+
// same way the config+accessToken branch below maps it, otherwise every
|
|
365
|
+
// field on the returned "CustomerUser" is silently undefined.
|
|
366
|
+
const data = await apiRequest<{ user: AuthUserResponse }>('/api/auth/me', {
|
|
367
|
+
credentials: 'include',
|
|
368
|
+
fallbackErrorMessage: 'Failed to fetch profile',
|
|
369
|
+
})
|
|
370
|
+
return mapAuthUserToCustomer(data.user)
|
|
218
371
|
}
|
|
219
372
|
|
|
220
|
-
const
|
|
221
|
-
headers:
|
|
222
|
-
|
|
223
|
-
},
|
|
373
|
+
const data = await apiRequest<AuthUserResponse>(`${config.backendUrls.auth}/auth/profile/`, {
|
|
374
|
+
headers: authHeaders(config, accessToken),
|
|
375
|
+
fallbackErrorMessage: 'Failed to fetch profile',
|
|
224
376
|
})
|
|
225
|
-
|
|
226
|
-
if (!response.ok) {
|
|
227
|
-
throw new Error('Failed to fetch profile')
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const data = await response.json()
|
|
231
377
|
return mapAuthUserToCustomer(data)
|
|
232
378
|
}
|
|
233
379
|
|
|
234
380
|
/** GET /auth/profile/ - alias for getMe for backward compatibility */
|
|
235
381
|
export async function getAuthUser(config?: SdkConfig, accessToken?: string): Promise<AuthUserResponse> {
|
|
236
382
|
if (!config || !accessToken) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
383
|
+
const data = await apiRequest<{ user: AuthUserResponse }>('/api/auth/me', {
|
|
384
|
+
credentials: 'include',
|
|
385
|
+
fallbackErrorMessage: 'Failed to fetch auth user',
|
|
386
|
+
})
|
|
240
387
|
return data.user
|
|
241
388
|
}
|
|
242
389
|
|
|
243
|
-
|
|
244
|
-
headers:
|
|
245
|
-
|
|
246
|
-
},
|
|
390
|
+
return apiRequest<AuthUserResponse>(`${config.backendUrls.auth}/auth/profile/`, {
|
|
391
|
+
headers: authHeaders(config, accessToken),
|
|
392
|
+
fallbackErrorMessage: 'Failed to fetch auth user',
|
|
247
393
|
})
|
|
248
|
-
|
|
249
|
-
if (!response.ok) {
|
|
250
|
-
throw new Error('Failed to fetch auth user')
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const data = await response.json()
|
|
254
|
-
return data
|
|
255
394
|
}
|
|
256
395
|
|
|
257
396
|
/** PUT /auth/profile/ — update firstname, lastname, phone */
|
|
@@ -263,35 +402,148 @@ export async function updateProfile(
|
|
|
263
402
|
const isArg1Config = typeof configOrPayload === 'object' && configOrPayload !== null && 'businessId' in configOrPayload
|
|
264
403
|
if (!isArg1Config) {
|
|
265
404
|
const payload = configOrPayload as UpdateProfilePayload
|
|
266
|
-
|
|
405
|
+
return apiRequest<CustomerUser>('/api/auth/profile', {
|
|
267
406
|
method: 'PUT',
|
|
268
|
-
headers:
|
|
407
|
+
headers: JSON_HEADERS,
|
|
269
408
|
credentials: 'include',
|
|
270
409
|
body: JSON.stringify(payload),
|
|
410
|
+
fallbackErrorMessage: 'Failed to update profile',
|
|
271
411
|
})
|
|
272
|
-
if (!response.ok) throw new Error('Failed to update profile')
|
|
273
|
-
return response.json()
|
|
274
412
|
}
|
|
275
413
|
|
|
276
414
|
const config = configOrPayload as SdkConfig
|
|
277
415
|
const accessToken = accessTokenOrPayload as string
|
|
278
416
|
const payload = payloadParam as UpdateProfilePayload
|
|
279
417
|
|
|
280
|
-
|
|
418
|
+
// REAL BUG FIXED (2026-09-20): hardcoded generic message discarded real
|
|
419
|
+
// CustomerProfileUpdateSerializer validation errors (e.g. invalid phone
|
|
420
|
+
// format) — apiRequest's shared extractor handles this DRF dict shape.
|
|
421
|
+
const data = await apiRequest<AuthUserResponse>(`${config.backendUrls.auth}/auth/profile/`, {
|
|
281
422
|
method: 'PUT',
|
|
282
|
-
headers: {
|
|
283
|
-
'Content-Type': 'application/json',
|
|
284
|
-
'Authorization': `Bearer ${accessToken}`,
|
|
285
|
-
},
|
|
423
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
286
424
|
body: JSON.stringify(payload),
|
|
425
|
+
fallbackErrorMessage: 'Failed to update profile',
|
|
287
426
|
})
|
|
427
|
+
return mapAuthUserToCustomer(data)
|
|
428
|
+
}
|
|
288
429
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
430
|
+
/**
|
|
431
|
+
* POST /auth/change-password/ — identity-mode-agnostic; the backend
|
|
432
|
+
* branches on the JWT's identity_mode (kasuvia-auth/authentication/api/v1/
|
|
433
|
+
* views/core.py:353, ChangePasswordView) to mutate the right underlying
|
|
434
|
+
* profile (customer/staff/owner). Used here for a logged-in storefront
|
|
435
|
+
* customer. `old_password` may be omitted only if the account genuinely has
|
|
436
|
+
* no password yet (a social/passwordless-only customer setting one for the
|
|
437
|
+
* first time) — the backend itself enforces this, not the frontend.
|
|
438
|
+
*/
|
|
439
|
+
export async function changePassword(
|
|
440
|
+
config: SdkConfig,
|
|
441
|
+
accessToken: string,
|
|
442
|
+
payload: ChangePasswordPayload
|
|
443
|
+
): Promise<{ detail: string }> {
|
|
444
|
+
return apiRequest<{ detail: string }>(`${config.backendUrls.auth}/auth/change-password/`, {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
447
|
+
body: JSON.stringify(payload),
|
|
448
|
+
fallbackErrorMessage: 'Failed to change password',
|
|
449
|
+
})
|
|
450
|
+
}
|
|
292
451
|
|
|
293
|
-
|
|
294
|
-
|
|
452
|
+
/**
|
|
453
|
+
* GET /auth/storefront/business-info/
|
|
454
|
+
* Public, unauthenticated — only needs the storefront key (no access token,
|
|
455
|
+
* no X-Business-ID; PublicBusinessInfoView resolves the business from the
|
|
456
|
+
* key itself). Real gap fixed 2026-09-17: this endpoint existed on the
|
|
457
|
+
* backend with zero SDK coverage.
|
|
458
|
+
*/
|
|
459
|
+
export async function getStorefrontBusinessInfo(config: SdkConfig): Promise<StorefrontBusinessInfo> {
|
|
460
|
+
// PublicBusinessInfoView resolves the business from X-Storefront-Key alone
|
|
461
|
+
// and never reads X-Business-ID — storefrontHeaders() sends both anyway
|
|
462
|
+
// for consistency with every other call site; the extra header is inert
|
|
463
|
+
// here (verified against the real view before making this change).
|
|
464
|
+
return apiRequest<StorefrontBusinessInfo>(`${config.backendUrls.auth}/auth/storefront/business-info/`, {
|
|
465
|
+
headers: storefrontHeaders(config),
|
|
466
|
+
fallbackErrorMessage: 'Failed to fetch business info',
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export interface PresignedUploadUrlParams {
|
|
471
|
+
filename: string
|
|
472
|
+
content_type: string
|
|
473
|
+
business_id: string
|
|
474
|
+
context: string
|
|
475
|
+
entity_id: string
|
|
476
|
+
owner_id?: string
|
|
477
|
+
affiliation?: string
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export interface PresignedUploadUrlResponse {
|
|
481
|
+
presigned_url: string
|
|
482
|
+
storage_path: string
|
|
483
|
+
bucket: string
|
|
484
|
+
expires_in: number
|
|
485
|
+
asset_id?: string
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* POST /auth/media/presigned-url/ — kasuvia-auth's thin proxy in front of
|
|
490
|
+
* kasuvia-media-system. Params are sent as a query string (not a JSON
|
|
491
|
+
* body) to match every other caller of this endpoint.
|
|
492
|
+
*/
|
|
493
|
+
export async function getPresignedUploadUrl(
|
|
494
|
+
config: SdkConfig,
|
|
495
|
+
accessToken: string,
|
|
496
|
+
params: PresignedUploadUrlParams
|
|
497
|
+
): Promise<PresignedUploadUrlResponse> {
|
|
498
|
+
const query = new URLSearchParams(
|
|
499
|
+
Object.entries(params).filter(([, v]) => v !== undefined) as [string, string][]
|
|
500
|
+
)
|
|
501
|
+
// Bearer-only, deliberately not storefrontHeaders() — this endpoint's real
|
|
502
|
+
// header contract was never verified to also accept/require X-Storefront-
|
|
503
|
+
// Key/X-Business-ID, unlike getStorefrontBusinessInfo above.
|
|
504
|
+
return apiRequest<PresignedUploadUrlResponse>(
|
|
505
|
+
`${config.backendUrls.auth}/auth/media/presigned-url/?${query.toString()}`,
|
|
506
|
+
{
|
|
507
|
+
method: 'POST',
|
|
508
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
509
|
+
fallbackErrorMessage: 'Failed to get presigned upload URL',
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export interface ConfirmMediaUploadParams {
|
|
515
|
+
storage_path: string
|
|
516
|
+
business_id: string
|
|
517
|
+
entity_id: string
|
|
518
|
+
original_filename: string
|
|
519
|
+
content_type: string
|
|
520
|
+
size: number
|
|
521
|
+
context: string
|
|
522
|
+
owner_id?: string
|
|
523
|
+
affiliation?: string
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* POST /auth/media/confirm-upload/ — confirms a completed direct-to-R2
|
|
528
|
+
* upload and returns the resulting MediaAsset. Params are sent as a query
|
|
529
|
+
* string (not a JSON body) to match every other caller of this endpoint.
|
|
530
|
+
*/
|
|
531
|
+
export async function confirmMediaUpload(
|
|
532
|
+
config: SdkConfig,
|
|
533
|
+
accessToken: string,
|
|
534
|
+
params: ConfirmMediaUploadParams
|
|
535
|
+
): Promise<MediaAsset> {
|
|
536
|
+
const query = new URLSearchParams(
|
|
537
|
+
Object.entries(params).filter(([, v]) => v !== undefined) as [string, string][]
|
|
538
|
+
)
|
|
539
|
+
return apiRequest<MediaAsset>(
|
|
540
|
+
`${config.backendUrls.auth}/auth/media/confirm-upload/?${query.toString()}`,
|
|
541
|
+
{
|
|
542
|
+
method: 'POST',
|
|
543
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
544
|
+
fallbackErrorMessage: 'Failed to confirm upload',
|
|
545
|
+
}
|
|
546
|
+
)
|
|
295
547
|
}
|
|
296
548
|
|
|
297
549
|
/**
|
|
@@ -312,32 +564,17 @@ export async function silentRefresh(
|
|
|
312
564
|
const user = buildCustomerFromClaims(claims)
|
|
313
565
|
const provider = user.provider
|
|
314
566
|
return { user, provider, newAccessToken: data.access, newRefreshToken: data.refresh }
|
|
315
|
-
} catch {
|
|
567
|
+
} catch (err) {
|
|
568
|
+
// REAL BUG FIXED (2026-09-20): bare `catch {}` silently discarded the
|
|
569
|
+
// real reason (network failure vs. an actually-revoked/expired refresh
|
|
570
|
+
// token) — `null` here correctly means "not currently logged in" to the
|
|
571
|
+
// caller (a legitimate outcome, not swallowed), but CLAUDE.md still
|
|
572
|
+
// requires the failure itself be logged, not silently dropped.
|
|
573
|
+
console.error('[auth.service] Silent token refresh failed:', err)
|
|
316
574
|
return null
|
|
317
575
|
}
|
|
318
576
|
}
|
|
319
577
|
|
|
320
|
-
// ── Helper Functions ─────────────────────────────────────────────────────────
|
|
321
|
-
|
|
322
|
-
function buildFullName(firstname?: string, lastname?: string): string {
|
|
323
|
-
if (firstname && lastname) return `${firstname} ${lastname}`
|
|
324
|
-
return firstname || lastname || ''
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function splitFullName(fullName?: string): { firstname: string; lastname: string } {
|
|
328
|
-
if (!fullName) return { firstname: '', lastname: '' }
|
|
329
|
-
|
|
330
|
-
const parts = fullName.trim().split(/\s+/)
|
|
331
|
-
if (parts.length === 1) {
|
|
332
|
-
return { firstname: parts[0], lastname: '' }
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
firstname: parts[0],
|
|
337
|
-
lastname: parts.slice(1).join(' ')
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
578
|
/**
|
|
342
579
|
* Store tokens in localStorage (client-side utility)
|
|
343
580
|
* This is a convenience function for storefronts to store auth tokens
|
|
@@ -371,3 +608,56 @@ export function getStoredTokens(): { accessToken: string | null; refreshToken: s
|
|
|
371
608
|
refreshToken: localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN),
|
|
372
609
|
}
|
|
373
610
|
}
|
|
611
|
+
|
|
612
|
+
// ── Customer saved-address book ─────────────────────────────────────────────
|
|
613
|
+
// REAL FEATURE WIRED (2026-09-22): CustomerProfile.address_book existed as a
|
|
614
|
+
// backend JSONField with no real write path anywhere (see
|
|
615
|
+
// CustomerAddressListView's own doc comment in kasuvia-auth). This is the
|
|
616
|
+
// SDK-side coverage for the real, now-validated endpoints.
|
|
617
|
+
|
|
618
|
+
/** GET /auth/profile/addresses/ */
|
|
619
|
+
export async function getCustomerAddresses(config: SdkConfig, accessToken: string): Promise<CustomerAddress[]> {
|
|
620
|
+
const data = await apiRequest<{ results: CustomerAddress[] }>(`${config.backendUrls.auth}/auth/profile/addresses/`, {
|
|
621
|
+
headers: authHeaders(config, accessToken),
|
|
622
|
+
fallbackErrorMessage: 'Failed to fetch saved addresses',
|
|
623
|
+
})
|
|
624
|
+
return data.results
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** POST /auth/profile/addresses/ */
|
|
628
|
+
export async function createCustomerAddress(
|
|
629
|
+
config: SdkConfig,
|
|
630
|
+
accessToken: string,
|
|
631
|
+
payload: CreateCustomerAddressPayload
|
|
632
|
+
): Promise<CustomerAddress> {
|
|
633
|
+
return apiRequest<CustomerAddress>(`${config.backendUrls.auth}/auth/profile/addresses/`, {
|
|
634
|
+
method: 'POST',
|
|
635
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
636
|
+
body: JSON.stringify(payload),
|
|
637
|
+
fallbackErrorMessage: 'Failed to save address',
|
|
638
|
+
})
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** PATCH /auth/profile/addresses/{id}/ */
|
|
642
|
+
export async function updateCustomerAddress(
|
|
643
|
+
config: SdkConfig,
|
|
644
|
+
accessToken: string,
|
|
645
|
+
addressId: string,
|
|
646
|
+
payload: UpdateCustomerAddressPayload
|
|
647
|
+
): Promise<CustomerAddress> {
|
|
648
|
+
return apiRequest<CustomerAddress>(`${config.backendUrls.auth}/auth/profile/addresses/${addressId}/`, {
|
|
649
|
+
method: 'PATCH',
|
|
650
|
+
headers: { ...JSON_HEADERS, ...authHeaders(config, accessToken) },
|
|
651
|
+
body: JSON.stringify(payload),
|
|
652
|
+
fallbackErrorMessage: 'Failed to update address',
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** DELETE /auth/profile/addresses/{id}/ */
|
|
657
|
+
export async function deleteCustomerAddress(config: SdkConfig, accessToken: string, addressId: string): Promise<void> {
|
|
658
|
+
await apiRequest(`${config.backendUrls.auth}/auth/profile/addresses/${addressId}/`, {
|
|
659
|
+
method: 'DELETE',
|
|
660
|
+
headers: authHeaders(config, accessToken),
|
|
661
|
+
fallbackErrorMessage: 'Failed to delete address',
|
|
662
|
+
})
|
|
663
|
+
}
|