@faststore/core 4.5.0-dev.0 → 4.5.0-dev.1

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 (61) hide show
  1. package/.turbo/turbo-generate.log +4 -4
  2. package/.turbo/turbo-test.log +132 -60
  3. package/@generated/cached-operations.json +2 -0
  4. package/@generated/gql.ts +18 -0
  5. package/@generated/graphql.ts +68 -0
  6. package/@generated/persisted-documents.json +3 -0
  7. package/@generated/schema.graphql +41 -0
  8. package/CHANGELOG.md +20 -0
  9. package/cms/faststore/components/cms_component__recommendationshelf.jsonc +86 -0
  10. package/cms/faststore/schema.json +89 -0
  11. package/discovery.config.default.js +3 -0
  12. package/discovery.config.js +10 -1
  13. package/discovery.config.local.example.js +62 -0
  14. package/package.json +6 -6
  15. package/public/icons.svg +4 -0
  16. package/src/Layout.tsx +6 -0
  17. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +376 -0
  18. package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/index.ts +2 -0
  19. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +51 -26
  20. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +55 -0
  21. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +45 -5
  22. package/src/components/account/Drawer/OrganizationDrawer/index.ts +5 -0
  23. package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +452 -54
  24. package/src/components/cms/global/Components.ts +10 -0
  25. package/src/components/sections/ProductGallery/ProductGallery.tsx +1 -1
  26. package/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +223 -0
  27. package/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +111 -0
  28. package/src/components/sections/RecommendationShelf/index.ts +1 -0
  29. package/src/components/sections/RecommendationShelf/section.module.scss +49 -0
  30. package/src/components/sections/RecommendationShelf/useRecommendationUserId.ts +49 -0
  31. package/src/components/sections/RecommendationShelf/useRecommendations.ts +55 -0
  32. package/src/components/sections/RecommendationShelf/vrn.ts +55 -0
  33. package/src/pages/[slug]/p.tsx +45 -0
  34. package/src/sdk/account/changeContractToken.ts +119 -0
  35. package/src/sdk/account/clearPersistedSessionState.ts +26 -0
  36. package/src/sdk/account/refreshToken.ts +21 -8
  37. package/src/sdk/account/useAvailableContracts.ts +49 -0
  38. package/src/sdk/account/useSwitchContract.ts +60 -0
  39. package/src/sdk/analytics/hooks/useStartRecommendationSession.ts +96 -0
  40. package/src/sdk/analytics/utils.ts +9 -0
  41. package/src/utils/retry.ts +69 -0
  42. package/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +235 -0
  43. package/test/components/RecommendationShelf/useRecommendationUserId.test.ts +69 -0
  44. package/test/components/RecommendationShelf/useRecommendations.test.ts +52 -0
  45. package/test/components/RecommendationShelf/vrn.test.ts +56 -0
  46. package/test/components/account/ContractSwitcher.browser.test.tsx +236 -0
  47. package/test/components/account/OrganizationDrawer.browser.test.tsx +197 -0
  48. package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +54 -0
  49. package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +55 -0
  50. package/test/components/account/doLogout.browser.test.ts +102 -0
  51. package/test/sdk/account/changeContractToken.browser.test.ts +157 -0
  52. package/test/sdk/account/clearPersistedSessionState.browser.test.ts +41 -0
  53. package/test/sdk/account/clearPersistedSessionState.test.ts +20 -0
  54. package/test/sdk/account/refreshToken.browser.test.ts +34 -0
  55. package/test/sdk/account/refreshToken.test.ts +87 -0
  56. package/test/sdk/account/useAvailableContracts.browser.test.ts +124 -0
  57. package/test/sdk/account/useSwitchContract.browser.test.ts +111 -0
  58. package/test/sdk/analytics/useStartRecommendationSession.test.ts +69 -0
  59. package/test/sdk/analytics/utils.test.ts +23 -0
  60. package/test/server/index.test.ts +3 -0
  61. package/test/utils/retry.test.ts +59 -0
@@ -0,0 +1,376 @@
1
+ import { Button, Icon, IconButton, Loader } from '@faststore/ui'
2
+ import { useState, type ReactNode } from 'react'
3
+
4
+ import { useAvailableContracts } from 'src/sdk/account/useAvailableContracts'
5
+ import { useSwitchContract } from 'src/sdk/account/useSwitchContract'
6
+ import { useSession } from 'src/sdk/session'
7
+
8
+ export type ContractSwitcherProps = {
9
+ /** Returns to the drawer menu view (also used by Cancel). */
10
+ onBack: () => void
11
+ /** Closes the whole drawer. */
12
+ onClose: () => void
13
+ }
14
+
15
+ /**
16
+ * Contract that can be displayed by the switcher. `isDefault` is optional so the
17
+ * component compiles against both the local mock and the real
18
+ * `useAvailableContracts` hook (which does not expose a default yet).
19
+ */
20
+ type SwitcherContract = {
21
+ id: string
22
+ corporateName: string
23
+ isActive: boolean
24
+ isDefault?: boolean
25
+ }
26
+
27
+ const initialOf = (name: string) => name.trim().charAt(0).toUpperCase()
28
+
29
+ type ContractSwitcherContentProps = {
30
+ loading: boolean
31
+ error: Error | null
32
+ currentContract: SwitcherContract | null
33
+ alternatives: SwitcherContract[]
34
+ ordered: SwitcherContract[]
35
+ search: string
36
+ selectedId: string | null
37
+ switchError: Error | null
38
+ onSearchChange: (value: string) => void
39
+ onSearchClear: () => void
40
+ onSelectContract: (contractId: string) => void
41
+ }
42
+
43
+ const ContractSwitcherLoading = () => (
44
+ <div data-fs-contract-switcher-loading aria-busy="true">
45
+ <Loader />
46
+ </div>
47
+ )
48
+
49
+ const ContractSwitcherLoadError = () => (
50
+ <div data-fs-contract-switcher-message role="alert">
51
+ <p>We couldn't load your contracts. Please try again.</p>
52
+ </div>
53
+ )
54
+
55
+ const ContractSwitcherCurrentSession = ({
56
+ currentContract,
57
+ }: {
58
+ currentContract: SwitcherContract | null
59
+ }) => (
60
+ <div data-fs-contract-switcher-current>
61
+ <span data-fs-contract-switcher-label>Current session</span>
62
+ {currentContract && (
63
+ <div data-fs-contract-switcher-current-card>
64
+ <span data-fs-contract-switcher-avatar aria-hidden="true">
65
+ {initialOf(currentContract.corporateName)}
66
+ </span>
67
+ <span data-fs-contract-switcher-option-name>
68
+ {currentContract.corporateName}
69
+ </span>
70
+ {currentContract.isDefault && (
71
+ <Icon
72
+ data-fs-contract-switcher-default
73
+ name="Star"
74
+ width={20}
75
+ height={20}
76
+ />
77
+ )}
78
+ </div>
79
+ )}
80
+ </div>
81
+ )
82
+
83
+ const ContractSwitcherEmptyAlternatives = () => (
84
+ <div data-fs-contract-switcher-message data-fs-contract-switcher-empty>
85
+ <p>No other contracts are available for your organization.</p>
86
+ </div>
87
+ )
88
+
89
+ type ContractSwitcherAlternativesProps = {
90
+ alternatives: SwitcherContract[]
91
+ ordered: SwitcherContract[]
92
+ search: string
93
+ selectedId: string | null
94
+ onSearchChange: (value: string) => void
95
+ onSearchClear: () => void
96
+ onSelectContract: (contractId: string) => void
97
+ }
98
+
99
+ const ContractSwitcherSearch = ({
100
+ search,
101
+ onSearchChange,
102
+ onSearchClear,
103
+ }: Pick<
104
+ ContractSwitcherAlternativesProps,
105
+ 'search' | 'onSearchChange' | 'onSearchClear'
106
+ >) => (
107
+ <div data-fs-contract-switcher-search>
108
+ <Icon name="MagnifyingGlass" width={20} height={20} />
109
+ <input
110
+ type="text"
111
+ value={search}
112
+ placeholder="Search"
113
+ aria-label="Search contracts"
114
+ onChange={(event) => onSearchChange(event.target.value)}
115
+ />
116
+ {search && (
117
+ <button
118
+ type="button"
119
+ data-fs-contract-switcher-search-clear
120
+ aria-label="Clear search"
121
+ onClick={onSearchClear}
122
+ >
123
+ <Icon name="X" width={18} height={18} />
124
+ </button>
125
+ )}
126
+ </div>
127
+ )
128
+
129
+ const ContractSwitcherOptionList = ({
130
+ ordered,
131
+ selectedId,
132
+ onSelectContract,
133
+ }: Pick<
134
+ ContractSwitcherAlternativesProps,
135
+ 'ordered' | 'selectedId' | 'onSelectContract'
136
+ >) => (
137
+ <ul data-fs-contract-switcher-list>
138
+ {ordered.map((contract) => {
139
+ const isSelected = selectedId === contract.id
140
+
141
+ return (
142
+ <li key={contract.id} data-fs-contract-switcher-item>
143
+ <button
144
+ type="button"
145
+ data-fs-contract-switcher-option
146
+ data-fs-contract-switcher-option-selected={isSelected}
147
+ aria-pressed={isSelected}
148
+ onClick={() => onSelectContract(contract.id)}
149
+ >
150
+ <span data-fs-contract-switcher-avatar aria-hidden="true">
151
+ {initialOf(contract.corporateName)}
152
+ </span>
153
+ <span data-fs-contract-switcher-option-name>
154
+ {contract.corporateName}
155
+ </span>
156
+ {contract.isDefault && (
157
+ <Icon
158
+ data-fs-contract-switcher-default
159
+ name="Star"
160
+ width={20}
161
+ height={20}
162
+ />
163
+ )}
164
+ </button>
165
+ </li>
166
+ )
167
+ })}
168
+ </ul>
169
+ )
170
+
171
+ const ContractSwitcherAlternatives = ({
172
+ alternatives,
173
+ ordered,
174
+ search,
175
+ selectedId,
176
+ onSearchChange,
177
+ onSearchClear,
178
+ onSelectContract,
179
+ }: ContractSwitcherAlternativesProps) => {
180
+ if (alternatives.length === 0) {
181
+ return <ContractSwitcherEmptyAlternatives />
182
+ }
183
+
184
+ let listContent: ReactNode
185
+ if (ordered.length === 0) {
186
+ listContent = (
187
+ <div data-fs-contract-switcher-message>
188
+ <p>No contracts match your search.</p>
189
+ </div>
190
+ )
191
+ } else {
192
+ listContent = (
193
+ <ContractSwitcherOptionList
194
+ ordered={ordered}
195
+ selectedId={selectedId}
196
+ onSelectContract={onSelectContract}
197
+ />
198
+ )
199
+ }
200
+
201
+ return (
202
+ <>
203
+ <p data-fs-contract-switcher-count>
204
+ Select one of <strong>{alternatives.length}</strong> available
205
+ contracts:
206
+ </p>
207
+ <ContractSwitcherSearch
208
+ search={search}
209
+ onSearchChange={onSearchChange}
210
+ onSearchClear={onSearchClear}
211
+ />
212
+ {listContent}
213
+ </>
214
+ )
215
+ }
216
+
217
+ const ContractSwitcherSwitchError = () => (
218
+ <div data-fs-contract-switcher-message role="alert">
219
+ <p>
220
+ We couldn't switch your contract. The previous contract is still active.
221
+ </p>
222
+ </div>
223
+ )
224
+
225
+ const ContractSwitcherContent = ({
226
+ loading,
227
+ error,
228
+ currentContract,
229
+ alternatives,
230
+ ordered,
231
+ search,
232
+ selectedId,
233
+ switchError,
234
+ onSearchChange,
235
+ onSearchClear,
236
+ onSelectContract,
237
+ }: ContractSwitcherContentProps) => {
238
+ if (loading) {
239
+ return <ContractSwitcherLoading />
240
+ }
241
+
242
+ if (error) {
243
+ return <ContractSwitcherLoadError />
244
+ }
245
+
246
+ return (
247
+ <>
248
+ <ContractSwitcherCurrentSession currentContract={currentContract} />
249
+ <ContractSwitcherAlternatives
250
+ alternatives={alternatives}
251
+ ordered={ordered}
252
+ search={search}
253
+ selectedId={selectedId}
254
+ onSearchChange={onSearchChange}
255
+ onSearchClear={onSearchClear}
256
+ onSelectContract={onSelectContract}
257
+ />
258
+ {switchError && <ContractSwitcherSwitchError />}
259
+ </>
260
+ )
261
+ }
262
+
263
+ /**
264
+ * Sub-view of the OrganizationDrawer that lets a B2B buyer change the active
265
+ * contract within their Organization Unit (REQ-03..06). Replaces the whole
266
+ * drawer body while open: own header (back / title / close), the current
267
+ * session, a searchable list of alternatives, and a Cancel / Confirm footer.
268
+ */
269
+ export const ContractSwitcher = ({
270
+ onBack,
271
+ onClose,
272
+ }: ContractSwitcherProps) => {
273
+ const { contracts, loading, error } = useAvailableContracts(true)
274
+ const { b2b } = useSession()
275
+ const {
276
+ switchContract,
277
+ loading: switching,
278
+ error: switchError,
279
+ enabled: switchEnabled,
280
+ } = useSwitchContract()
281
+
282
+ const [search, setSearch] = useState('')
283
+ const [selectedId, setSelectedId] = useState<string | null>(null)
284
+
285
+ const allContracts: SwitcherContract[] = contracts
286
+ const activeContractId = b2b?.customerId?.trim() ?? ''
287
+ const currentContract =
288
+ (activeContractId
289
+ ? allContracts.find((contract) => contract.id === activeContractId)
290
+ : null) ??
291
+ allContracts.find((contract) => contract.isActive) ??
292
+ allContracts[0] ??
293
+ null
294
+
295
+ const alternatives = allContracts.filter(
296
+ (contract) => contract.id !== currentContract?.id
297
+ )
298
+
299
+ const query = search.toLowerCase().trim()
300
+ const filtered = query
301
+ ? alternatives.filter((contract) =>
302
+ contract.corporateName.toLowerCase().includes(query)
303
+ )
304
+ : alternatives
305
+
306
+ const ordered = [...filtered].sort((a, b) => {
307
+ if (a.isDefault && !b.isDefault) return -1
308
+ if (b.isDefault && !a.isDefault) return 1
309
+ return a.corporateName.localeCompare(b.corporateName)
310
+ })
311
+
312
+ const canConfirm =
313
+ switchEnabled &&
314
+ selectedId !== null &&
315
+ selectedId !== currentContract?.id &&
316
+ ordered.some((contract) => contract.id === selectedId) &&
317
+ !switching
318
+
319
+ const handleConfirm = async () => {
320
+ if (!selectedId) {
321
+ return
322
+ }
323
+
324
+ await switchContract(selectedId)
325
+ }
326
+
327
+ return (
328
+ <section data-fs-contract-switcher>
329
+ <header data-fs-contract-switcher-header>
330
+ <IconButton
331
+ data-fs-contract-switcher-back
332
+ aria-label="Back to account menu"
333
+ icon={<Icon name="ArrowLeft" width={20} height={20} />}
334
+ onClick={onBack}
335
+ />
336
+ <h2 data-fs-contract-switcher-title>Change contract</h2>
337
+ <IconButton
338
+ data-fs-contract-switcher-close
339
+ aria-label="Close"
340
+ icon={<Icon name="X" width={20} height={20} />}
341
+ onClick={onClose}
342
+ />
343
+ </header>
344
+
345
+ <div data-fs-contract-switcher-content>
346
+ <ContractSwitcherContent
347
+ loading={loading}
348
+ error={error}
349
+ currentContract={currentContract}
350
+ alternatives={alternatives}
351
+ ordered={ordered}
352
+ search={search}
353
+ selectedId={selectedId}
354
+ switchError={switchError}
355
+ onSearchChange={setSearch}
356
+ onSearchClear={() => setSearch('')}
357
+ onSelectContract={setSelectedId}
358
+ />
359
+ </div>
360
+
361
+ <footer data-fs-contract-switcher-footer>
362
+ <Button variant="tertiary" onClick={onBack}>
363
+ Cancel
364
+ </Button>
365
+ <Button
366
+ variant="primary"
367
+ disabled={!canConfirm}
368
+ loading={switching}
369
+ onClick={handleConfirm}
370
+ >
371
+ Confirm
372
+ </Button>
373
+ </footer>
374
+ </section>
375
+ )
376
+ }
@@ -0,0 +1,2 @@
1
+ export { ContractSwitcher } from './ContractSwitcher'
2
+ export type { ContractSwitcherProps } from './ContractSwitcher'
@@ -1,4 +1,5 @@
1
1
  import { SlideOver, useFadeEffect } from '@faststore/ui'
2
+ import { useEffect, useState } from 'react'
2
3
 
3
4
  import { getStoreURL } from 'src/sdk/localization/useLocalizationConfig'
4
5
  import { useSession } from 'src/sdk/session'
@@ -9,12 +10,15 @@ import {
9
10
  getVtexCookieNames,
10
11
  } from 'src/utils/clearCookies'
11
12
  import storeConfig from '../../../../../discovery.config'
12
- import { ProfileSummary } from '../ProfileSummary/ProfileSummary'
13
+ import { ContractSwitcher } from './ContractSwitcher'
13
14
  import { OrganizationDrawerBody } from './OrganizationDrawerBody'
15
+ import { OrganizationDrawerFooter } from './OrganizationDrawerFooter'
14
16
  import { OrganizationDrawerHeader } from './OrganizationDrawerHeader'
15
17
  import { setReloadAfterLogoutReturn } from './useReloadAfterLogoutReturn'
16
18
  import styles from './section.module.scss'
17
19
 
20
+ type OrganizationDrawerView = 'menu' | 'switch'
21
+
18
22
  type OrganizationDrawerProps = {
19
23
  isOpen: boolean
20
24
  closeDrawer: () => void
@@ -125,7 +129,9 @@ export const doLogout = async (_event?: unknown) => {
125
129
  } finally {
126
130
  setReloadAfterLogoutReturn()
127
131
  window.location.assign(
128
- `${storeConfig.secureSubdomain}/api/vtexid/pub/logout?scope=${storeConfig.api.storeId}&returnUrl=${getStoreURL()}`
132
+ `${storeConfig.secureSubdomain}/api/vtexid/pub/logout?scope=${
133
+ storeConfig.api.storeId
134
+ }&returnUrl=${getStoreURL()}`
129
135
  )
130
136
  }
131
137
  }
@@ -137,17 +143,29 @@ export const OrganizationDrawer = ({
137
143
  }: OrganizationDrawerProps) => {
138
144
  const { fade, fadeOut } = useFadeEffect()
139
145
  const { b2b, person } = useSession()
146
+ const [view, setView] = useState<OrganizationDrawerView>('menu')
147
+
148
+ useEffect(() => {
149
+ if (!isOpen) {
150
+ setView('menu')
151
+ }
152
+ }, [isOpen])
140
153
 
141
154
  const contractName =
142
- b2b?.contractName ??
143
- `${(person?.givenName ?? '').trim()} ${(person?.familyName ?? '').trim()}`.trim() ??
144
- ''
155
+ b2b?.contractName?.trim() ||
156
+ b2b?.unitName?.trim() ||
157
+ `${(person?.givenName ?? '').trim()}` ||
158
+ 'Organization'
145
159
 
146
160
  const contractUrl = b2b?.unitId
147
161
  ? `/pvt/organization-account/org-unit/${b2b?.unitId}`
148
162
  : null
149
163
 
150
164
  const isOrganizationManager = b2b?.organizationManager || false
165
+ // The switcher is only meaningful for B2B buyers tied to an Organization Unit.
166
+ // Buyers with a single (or no alternative) contract see the empty state inside
167
+ // the switcher; we don't fetch the contract count eagerly to protect TTFB.
168
+ const canSwitchContract = Boolean(b2b?.unitId)
151
169
  const isOrgMember = Boolean(b2b?.unitId)
152
170
 
153
171
  return (
@@ -163,27 +181,34 @@ export const OrganizationDrawer = ({
163
181
  className: `section ${styles.section} section-organization-drawer`,
164
182
  }}
165
183
  >
166
- <OrganizationDrawerHeader
167
- onCloseDrawer={closeDrawer}
168
- contractName={contractName}
169
- contractUrl={contractUrl}
170
- />
171
- <OrganizationDrawerBody
172
- isRepresentative={isRepresentative}
173
- isOrgMember={isOrgMember}
174
- />
175
- <footer data-fs-organization-drawer-footer-wrapper>
176
- <ProfileSummary
177
- showManageLink={isOrganizationManager}
178
- bordered={true}
179
- onLogoutClick={doLogout}
180
- person={{
181
- name: b2b?.userName ?? '',
182
- email: b2b?.userEmail ?? '',
183
- }}
184
- orgName={b2b?.unitName ?? ''}
185
- />
186
- </footer>
184
+ {view === 'switch' ? (
185
+ <ContractSwitcher onBack={() => setView('menu')} onClose={fadeOut} />
186
+ ) : (
187
+ <div data-fs-organization-drawer-menu>
188
+ <div data-fs-organization-drawer-menu-scroll>
189
+ <OrganizationDrawerHeader
190
+ onCloseDrawer={closeDrawer}
191
+ contractName={contractName}
192
+ contractUrl={contractUrl}
193
+ onChangeContract={
194
+ canSwitchContract ? () => setView('switch') : undefined
195
+ }
196
+ />
197
+ <OrganizationDrawerBody
198
+ isRepresentative={isRepresentative}
199
+ isOrgMember={isOrgMember}
200
+ />
201
+ </div>
202
+ <OrganizationDrawerFooter
203
+ orgName={b2b?.unitName ?? ''}
204
+ userName={b2b?.userName ?? ''}
205
+ userEmail={b2b?.userEmail ?? ''}
206
+ showManageLink={isOrganizationManager}
207
+ manageUrl={contractUrl ?? undefined}
208
+ onLogoutClick={doLogout}
209
+ />
210
+ </div>
211
+ )}
187
212
  </SlideOver>
188
213
  )
189
214
  }
@@ -0,0 +1,55 @@
1
+ import { Icon, IconButton, Link } from '@faststore/ui'
2
+
3
+ export interface OrganizationDrawerFooterProps {
4
+ orgName: string
5
+ userName: string
6
+ userEmail?: string
7
+ showManageLink?: boolean
8
+ manageUrl?: string
9
+ onLogoutClick?: () => void
10
+ }
11
+
12
+ export const OrganizationDrawerFooter = ({
13
+ orgName,
14
+ userName,
15
+ userEmail,
16
+ showManageLink,
17
+ manageUrl,
18
+ onLogoutClick,
19
+ }: OrganizationDrawerFooterProps) => {
20
+ return (
21
+ <div data-fs-organization-drawer-footer>
22
+ <div data-fs-organization-drawer-footer-org>
23
+ <div data-fs-organization-drawer-footer-org-info>
24
+ <Icon
25
+ data-fs-organization-drawer-footer-org-icon
26
+ name="FolderOpen"
27
+ width={18}
28
+ height={18}
29
+ />
30
+ <span data-fs-organization-drawer-footer-org-name>{orgName}</span>
31
+ </div>
32
+ {showManageLink && manageUrl && (
33
+ <Link href={manageUrl} data-fs-organization-drawer-footer-manage>
34
+ Manage
35
+ </Link>
36
+ )}
37
+ </div>
38
+
39
+ <div data-fs-organization-drawer-footer-user>
40
+ <div data-fs-organization-drawer-footer-user-info>
41
+ <p data-fs-organization-drawer-footer-user-name>{userName}</p>
42
+ {userEmail && (
43
+ <p data-fs-organization-drawer-footer-user-email>{userEmail}</p>
44
+ )}
45
+ </div>
46
+ <IconButton
47
+ data-fs-organization-drawer-footer-logout
48
+ aria-label="Log out"
49
+ icon={<Icon name="SignOut" width={20} height={20} />}
50
+ onClick={onLogoutClick}
51
+ />
52
+ </div>
53
+ </div>
54
+ )
55
+ }
@@ -1,18 +1,29 @@
1
- import { Link, SlideOverHeader } from '@faststore/ui'
1
+ import { Button, Icon, Link, SlideOverHeader } from '@faststore/ui'
2
2
  import type { ReactNode } from 'react'
3
3
 
4
4
  export type OrganizationDrawerHeaderProps = {
5
5
  onCloseDrawer?: () => void
6
6
  contractImage?: ReactNode
7
7
  contractName: string
8
- contractUrl: string
8
+ contractUrl: string | null
9
+ /**
10
+ * Marks the active contract as the Organization Unit's default (star).
11
+ */
12
+ isDefault?: boolean
13
+ /**
14
+ * When provided, renders the "Change" CTA next to the active contract,
15
+ * opening the contract switcher (Figma node 103-5434).
16
+ */
17
+ onChangeContract?: () => void
9
18
  }
10
19
 
11
20
  export const OrganizationDrawerHeader = ({
12
21
  contractUrl,
13
22
  contractName,
14
23
  contractImage,
24
+ isDefault,
15
25
  onCloseDrawer,
26
+ onChangeContract,
16
27
  }: OrganizationDrawerHeaderProps) => {
17
28
  return (
18
29
  <>
@@ -20,6 +31,7 @@ export const OrganizationDrawerHeader = ({
20
31
  <div data-fs-organization-drawer-header>
21
32
  <Link
22
33
  data-fs-organization-drawer-header-contract-link
34
+ aria-label={contractName}
23
35
  {...(contractUrl && { href: contractUrl })}
24
36
  >
25
37
  <div data-fs-organization-drawer-header-contract-image>
@@ -29,10 +41,38 @@ export const OrganizationDrawerHeader = ({
29
41
  </span>
30
42
  )}
31
43
  </div>
32
- <h1 data-fs-organization-drawer-header-contract-name>
33
- {contractName}
34
- </h1>
35
44
  </Link>
45
+ <div data-fs-organization-drawer-header-row>
46
+ <Link
47
+ data-fs-organization-drawer-header-contract-name-link
48
+ {...(contractUrl && { href: contractUrl })}
49
+ >
50
+ <h1 data-fs-organization-drawer-header-contract-name>
51
+ {contractName}
52
+ </h1>
53
+ </Link>
54
+ {(isDefault || onChangeContract) && (
55
+ <div data-fs-organization-drawer-header-actions>
56
+ {isDefault && (
57
+ <Icon
58
+ data-fs-organization-drawer-header-default
59
+ name="Star"
60
+ width={20}
61
+ height={20}
62
+ />
63
+ )}
64
+ {onChangeContract && (
65
+ <Button
66
+ data-fs-organization-drawer-header-change
67
+ variant="secondary"
68
+ onClick={onChangeContract}
69
+ >
70
+ Change
71
+ </Button>
72
+ )}
73
+ </div>
74
+ )}
75
+ </div>
36
76
  </div>
37
77
  </>
38
78
  )
@@ -1,3 +1,8 @@
1
1
  export { OrganizationDrawer } from './OrganizationDrawer'
2
2
  export { OrganizationDrawerBody } from './OrganizationDrawerBody'
3
+ export { OrganizationDrawerFooter } from './OrganizationDrawerFooter'
4
+ export type { OrganizationDrawerFooterProps } from './OrganizationDrawerFooter'
3
5
  export { OrganizationDrawerHeader } from './OrganizationDrawerHeader'
6
+ export type { OrganizationDrawerHeaderProps } from './OrganizationDrawerHeader'
7
+ export { ContractSwitcher } from './ContractSwitcher'
8
+ export type { ContractSwitcherProps } from './ContractSwitcher'