@faststore/core 4.6.0 → 4.7.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-generate.log +4 -4
- package/.turbo/turbo-test.log +105 -90
- package/@generated/cached-operations.json +1 -0
- package/@generated/gql.ts +6 -0
- package/@generated/graphql.ts +61 -0
- package/@generated/persisted-documents.json +1 -0
- package/@generated/schema.graphql +56 -0
- package/CHANGELOG.md +10 -0
- package/cms/faststore/my-account/components/cms_component__accountlistcards.jsonc +90 -0
- package/cms/faststore/my-account/pages/cms_content_type__myaccountcards.jsonc +18 -0
- package/package.json +6 -6
- package/public/icons.svg +1 -1
- package/src/components/account/Layout/Layout.tsx +4 -0
- package/src/components/account/cards/MyAccountListCards/MyAccountListCards.tsx +409 -0
- package/src/components/account/cards/MyAccountListCards/index.ts +11 -0
- package/src/components/account/cards/MyAccountListCards/myAccountListCardsLabels.ts +44 -0
- package/src/components/account/cards/MyAccountListCards/styles.module.scss +126 -0
- package/src/components/cms/account/Components.ts +2 -0
- package/src/components/sections/Account/AccountListCards.tsx +69 -0
- package/src/pages/pvt/account/cards/index.tsx +244 -0
- package/src/sdk/account/accountPageContext.ts +16 -0
- package/src/sdk/account/getB2BSessionClaims.ts +42 -0
- package/src/sdk/account/getMyAccountRoutes.ts +7 -0
- package/src/server/cms/myAccountDefaultSections.ts +2 -0
- package/src/utils/getCookie.ts +8 -1
- package/test/components/account/cards/MyAccountListCards.browser.test.tsx +301 -0
- package/test/components/sections/Account/AccountListCards.browser.test.tsx +57 -0
- package/test/pages/pvt/account/cards/index.test.ts +202 -0
- package/test/sdk/account/getB2BSessionClaims.test.ts +48 -0
- package/test/sdk/account/getMyAccountRoutes.test.ts +10 -0
- package/test/server/index.test.ts +2 -0
- package/test/utils/getCookie.test.ts +23 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type MyAccountListCardsSectionLabels = {
|
|
2
|
+
pageTitle?: string
|
|
3
|
+
personalTabLabel?: string
|
|
4
|
+
sharedTabLabel?: string
|
|
5
|
+
searchPlaceholder?: string
|
|
6
|
+
paginationOfLabel?: string
|
|
7
|
+
previousPageLabel?: string
|
|
8
|
+
nextPageLabel?: string
|
|
9
|
+
noPersonalCardsLabel?: string
|
|
10
|
+
noSharedCardsLabel?: string
|
|
11
|
+
noResultsLabel?: string
|
|
12
|
+
errorTitleLabel?: string
|
|
13
|
+
tryAgainLabel?: string
|
|
14
|
+
sharedCardsTooltipLabel?: string
|
|
15
|
+
defaultCardLabel?: string
|
|
16
|
+
/** Shown in place of the brand name when the service doesn't return one. */
|
|
17
|
+
genericCardLabel?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const defaultMyAccountListCardsLabels: Required<MyAccountListCardsSectionLabels> =
|
|
21
|
+
{
|
|
22
|
+
pageTitle: 'Cards',
|
|
23
|
+
personalTabLabel: 'Personal',
|
|
24
|
+
sharedTabLabel: 'Shared',
|
|
25
|
+
searchPlaceholder: 'Search',
|
|
26
|
+
paginationOfLabel: 'of',
|
|
27
|
+
previousPageLabel: 'Previous Page',
|
|
28
|
+
nextPageLabel: 'Next Page',
|
|
29
|
+
noPersonalCardsLabel: "You don't have any cards",
|
|
30
|
+
noSharedCardsLabel: 'Your organization has no shared cards',
|
|
31
|
+
noResultsLabel: 'No results found',
|
|
32
|
+
errorTitleLabel: "We couldn't load your cards",
|
|
33
|
+
tryAgainLabel: 'Try again',
|
|
34
|
+
sharedCardsTooltipLabel:
|
|
35
|
+
'These cards are shared by the organization you belong to. Changes can only be made by the administrator.',
|
|
36
|
+
defaultCardLabel: 'Default',
|
|
37
|
+
genericCardLabel: 'Card',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveMyAccountListCardsLabels(
|
|
41
|
+
labels?: MyAccountListCardsSectionLabels
|
|
42
|
+
): Required<MyAccountListCardsSectionLabels> {
|
|
43
|
+
return { ...defaultMyAccountListCardsLabels, ...labels }
|
|
44
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
@use "sass:meta";
|
|
2
|
+
|
|
3
|
+
.page {
|
|
4
|
+
@include meta.load-css("~@faststore/ui/src/components/organisms/EmptyState/styles.scss");
|
|
5
|
+
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--fs-spacing-4, 1.5rem);
|
|
9
|
+
|
|
10
|
+
[data-fs-list-cards-tabs-row] {
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: var(--fs-spacing-0-5, 0.25rem);
|
|
13
|
+
align-items: center;
|
|
14
|
+
border-bottom: 1px solid var(--fs-border-color, #d9d9d9);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[data-fs-list-cards-tabs] {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: var(--fs-spacing-4, 1.5rem);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[data-fs-list-cards-tab] {
|
|
23
|
+
display: inline-flex;
|
|
24
|
+
gap: var(--fs-spacing-0-5, 0.25rem);
|
|
25
|
+
align-items: center;
|
|
26
|
+
padding: var(--fs-spacing-1, 0.5rem) 0;
|
|
27
|
+
font: inherit;
|
|
28
|
+
color: var(--fs-text-secondary, #6f6f6f);
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
background: none;
|
|
31
|
+
border: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
[data-fs-list-cards-tab-active="true"] {
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
color: var(--fs-text-link, #0632c1);
|
|
37
|
+
border-bottom: 2px solid var(--fs-text-link, #0632c1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Shared-only rendering (spec my-account-cards-gating-plan): no tab bar to
|
|
41
|
+
// switch between, just a heading-style label + info tooltip above the grid.
|
|
42
|
+
[data-fs-list-cards-tabs-shared-only] {
|
|
43
|
+
display: flex;
|
|
44
|
+
gap: var(--fs-spacing-0-5, 0.25rem);
|
|
45
|
+
align-items: center;
|
|
46
|
+
padding-bottom: var(--fs-spacing-1, 0.5rem);
|
|
47
|
+
border-bottom: 1px solid var(--fs-border-color, #d9d9d9);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-fs-list-cards-tab-label] {
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
color: var(--fs-text-link, #0632c1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[data-fs-list-cards-controls] {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
gap: var(--fs-spacing-2, 1rem);
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[data-fs-list-cards-search-wrapper] {
|
|
64
|
+
flex: 1;
|
|
65
|
+
max-width: 25rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[data-fs-list-cards-pagination] {
|
|
69
|
+
display: flex;
|
|
70
|
+
gap: var(--fs-spacing-1, 0.5rem);
|
|
71
|
+
align-items: center;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[data-fs-list-cards-grid] {
|
|
76
|
+
display: grid;
|
|
77
|
+
grid-template-columns: repeat(auto-fill, 10rem);
|
|
78
|
+
gap: 1.25rem;
|
|
79
|
+
|
|
80
|
+
@media (max-width: 768px) {
|
|
81
|
+
grid-template-columns: repeat(2, 1fr);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
[data-fs-list-cards-item] {
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
gap: var(--fs-spacing-1, 0.5rem);
|
|
89
|
+
padding: var(--fs-spacing-2, 1rem);
|
|
90
|
+
border: 1px solid var(--fs-border-color, #d9d9d9);
|
|
91
|
+
border-radius: var(--fs-border-radius, 0.25rem);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[data-fs-list-cards-item-brand] {
|
|
95
|
+
font-size: 0.875rem;
|
|
96
|
+
font-weight: 700;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[data-fs-list-cards-item-label] {
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: var(--fs-spacing-0-5, 0.25rem);
|
|
102
|
+
align-items: center;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[data-fs-list-cards-item-default-badge] {
|
|
106
|
+
padding: 0.0625rem 0.5rem;
|
|
107
|
+
font-size: 0.75rem;
|
|
108
|
+
color: var(--fs-text-secondary, #6f6f6f);
|
|
109
|
+
border: 1px solid var(--fs-border-color, #d9d9d9);
|
|
110
|
+
border-radius: 999px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[data-fs-list-cards-item-number] {
|
|
114
|
+
font-size: 0.875rem;
|
|
115
|
+
color: var(--fs-text-secondary, #6f6f6f);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The shared EmptyState is designed for full-page empty states
|
|
119
|
+
// (min-height: 50vh) — shrink that here so it sits inline within this
|
|
120
|
+
// list section instead of pushing a huge block, while keeping its
|
|
121
|
+
// default centered content.
|
|
122
|
+
[data-fs-empty-state] {
|
|
123
|
+
min-height: auto;
|
|
124
|
+
padding: 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentType } from 'react'
|
|
2
2
|
|
|
3
|
+
import AccountListCards from 'src/components/sections/Account/AccountListCards'
|
|
3
4
|
import AccountNavigation from 'src/components/sections/Account/AccountNavigation'
|
|
4
5
|
import AccountOrderBudgets from 'src/components/sections/Account/AccountOrderBudgets'
|
|
5
6
|
import AccountOrderDelivery from 'src/components/sections/Account/AccountOrderDelivery'
|
|
@@ -25,6 +26,7 @@ const ACCOUNT_COMPONENTS: Record<string, ComponentType<any>> = {
|
|
|
25
26
|
[getComponentKey(AccountNavigation, 'AccountNavigation')]: AccountNavigation,
|
|
26
27
|
[getComponentKey(AccountProfile, 'AccountProfile')]: AccountProfile,
|
|
27
28
|
[getComponentKey(AccountOrdersList, 'AccountOrdersList')]: AccountOrdersList,
|
|
29
|
+
[getComponentKey(AccountListCards, 'AccountListCards')]: AccountListCards,
|
|
28
30
|
[getComponentKey(AccountUserDetails, 'AccountUserDetails')]:
|
|
29
31
|
AccountUserDetails,
|
|
30
32
|
[getComponentKey(AccountSecurity, 'AccountSecurity')]: AccountSecurity,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { MyAccountListCards } from 'src/components/account/cards/MyAccountListCards'
|
|
2
|
+
import type { MyAccountListCardsSectionLabels } from 'src/components/account/cards/MyAccountListCards/myAccountListCardsLabels'
|
|
3
|
+
import { defaultMyAccountListCardsLabels } from 'src/components/account/cards/MyAccountListCards/myAccountListCardsLabels'
|
|
4
|
+
import {
|
|
5
|
+
type AccountCardsPageData,
|
|
6
|
+
useAccountPageData,
|
|
7
|
+
} from 'src/sdk/account/accountPageContext'
|
|
8
|
+
import Section from '../Section'
|
|
9
|
+
|
|
10
|
+
export type AccountListCardsProps = MyAccountListCardsSectionLabels
|
|
11
|
+
|
|
12
|
+
const AccountListCards = ({
|
|
13
|
+
pageTitle = defaultMyAccountListCardsLabels.pageTitle,
|
|
14
|
+
personalTabLabel = defaultMyAccountListCardsLabels.personalTabLabel,
|
|
15
|
+
sharedTabLabel = defaultMyAccountListCardsLabels.sharedTabLabel,
|
|
16
|
+
searchPlaceholder = defaultMyAccountListCardsLabels.searchPlaceholder,
|
|
17
|
+
paginationOfLabel = defaultMyAccountListCardsLabels.paginationOfLabel,
|
|
18
|
+
previousPageLabel = defaultMyAccountListCardsLabels.previousPageLabel,
|
|
19
|
+
nextPageLabel = defaultMyAccountListCardsLabels.nextPageLabel,
|
|
20
|
+
noPersonalCardsLabel = defaultMyAccountListCardsLabels.noPersonalCardsLabel,
|
|
21
|
+
noSharedCardsLabel = defaultMyAccountListCardsLabels.noSharedCardsLabel,
|
|
22
|
+
noResultsLabel = defaultMyAccountListCardsLabels.noResultsLabel,
|
|
23
|
+
errorTitleLabel = defaultMyAccountListCardsLabels.errorTitleLabel,
|
|
24
|
+
tryAgainLabel = defaultMyAccountListCardsLabels.tryAgainLabel,
|
|
25
|
+
sharedCardsTooltipLabel = defaultMyAccountListCardsLabels.sharedCardsTooltipLabel,
|
|
26
|
+
defaultCardLabel = defaultMyAccountListCardsLabels.defaultCardLabel,
|
|
27
|
+
genericCardLabel = defaultMyAccountListCardsLabels.genericCardLabel,
|
|
28
|
+
}: AccountListCardsProps) => {
|
|
29
|
+
const {
|
|
30
|
+
personalCards,
|
|
31
|
+
sharedCards,
|
|
32
|
+
hasOrgAssociation,
|
|
33
|
+
canViewPersonalCards,
|
|
34
|
+
hasError,
|
|
35
|
+
} = useAccountPageData<AccountCardsPageData>()
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Section className="section-account-list-cards">
|
|
39
|
+
<MyAccountListCards
|
|
40
|
+
personalCards={personalCards ?? []}
|
|
41
|
+
sharedCards={sharedCards ?? []}
|
|
42
|
+
hasOrgAssociation={hasOrgAssociation}
|
|
43
|
+
canViewPersonalCards={canViewPersonalCards}
|
|
44
|
+
hasError={hasError}
|
|
45
|
+
labels={{
|
|
46
|
+
pageTitle,
|
|
47
|
+
personalTabLabel,
|
|
48
|
+
sharedTabLabel,
|
|
49
|
+
searchPlaceholder,
|
|
50
|
+
paginationOfLabel,
|
|
51
|
+
previousPageLabel,
|
|
52
|
+
nextPageLabel,
|
|
53
|
+
noPersonalCardsLabel,
|
|
54
|
+
noSharedCardsLabel,
|
|
55
|
+
noResultsLabel,
|
|
56
|
+
errorTitleLabel,
|
|
57
|
+
tryAgainLabel,
|
|
58
|
+
sharedCardsTooltipLabel,
|
|
59
|
+
defaultCardLabel,
|
|
60
|
+
genericCardLabel,
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
</Section>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
AccountListCards.$componentKey = 'AccountListCards'
|
|
68
|
+
|
|
69
|
+
export default AccountListCards
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { Locator, Section } from '@vtex/client-cms'
|
|
2
|
+
import type { GetServerSideProps } from 'next'
|
|
3
|
+
import { NextSeo } from 'next-seo'
|
|
4
|
+
import type { ComponentType } from 'react'
|
|
5
|
+
import { Layout } from 'src/components/account'
|
|
6
|
+
import RenderSections, {
|
|
7
|
+
RenderSectionsBase,
|
|
8
|
+
} from 'src/components/cms/RenderSections'
|
|
9
|
+
import ACCOUNT_COMPONENTS from 'src/components/cms/account/Components'
|
|
10
|
+
import GLOBAL_COMPONENTS from 'src/components/cms/global/Components'
|
|
11
|
+
import CUSTOM_COMPONENTS from 'src/customizations/src/components'
|
|
12
|
+
|
|
13
|
+
import { getGlobalSectionsData } from 'src/components/cms/GlobalSections'
|
|
14
|
+
|
|
15
|
+
import { gql } from '@generated/gql'
|
|
16
|
+
import type {
|
|
17
|
+
ServerListCardsQueryQuery,
|
|
18
|
+
ServerListCardsQueryQueryVariables,
|
|
19
|
+
} from '@generated/graphql'
|
|
20
|
+
import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
|
|
21
|
+
import type { AccountCardsPageData } from 'src/sdk/account/accountPageContext'
|
|
22
|
+
import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
|
|
23
|
+
import { getB2BSessionClaims } from 'src/sdk/account/getB2BSessionClaims'
|
|
24
|
+
import { getIsRepresentative } from 'src/sdk/account/getIsRepresentative'
|
|
25
|
+
import { execute } from 'src/server'
|
|
26
|
+
import { injectGlobalSections } from 'src/server/cms/global'
|
|
27
|
+
import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
|
|
28
|
+
import { fetchMyAccountPageContent } from 'src/server/cms/fetchMyAccountPageContent'
|
|
29
|
+
import { withLocaleValidationSSR } from 'src/utils/localization/withLocaleValidation'
|
|
30
|
+
import { getMyAccountRedirect } from 'src/utils/myAccountRedirect'
|
|
31
|
+
|
|
32
|
+
import storeConfig from 'discovery.config'
|
|
33
|
+
import PageProvider from 'src/sdk/overrides/PageProvider'
|
|
34
|
+
import { extractStatusFromError } from 'src/utils/utilities'
|
|
35
|
+
|
|
36
|
+
const COMPONENTS: Record<string, ComponentType<any>> = {
|
|
37
|
+
...GLOBAL_COMPONENTS,
|
|
38
|
+
...CUSTOM_COMPONENTS,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const query = gql(`
|
|
42
|
+
query ServerListCardsQuery {
|
|
43
|
+
listCreditCards {
|
|
44
|
+
list {
|
|
45
|
+
accountId
|
|
46
|
+
bin
|
|
47
|
+
cardNumber
|
|
48
|
+
cardLabel
|
|
49
|
+
paymentSystem
|
|
50
|
+
paymentSystemName
|
|
51
|
+
isDefault
|
|
52
|
+
isActive
|
|
53
|
+
origin
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
accountProfile {
|
|
57
|
+
name
|
|
58
|
+
}
|
|
59
|
+
hasAdHocCardAccess
|
|
60
|
+
}
|
|
61
|
+
`)
|
|
62
|
+
|
|
63
|
+
type ListCardsPageProps = {
|
|
64
|
+
pageSections: Section[]
|
|
65
|
+
navigationLabels: AccountNavigationLabels
|
|
66
|
+
accountPageData: AccountCardsPageData
|
|
67
|
+
} & MyAccountProps
|
|
68
|
+
|
|
69
|
+
export default function ListCardsPage({
|
|
70
|
+
globalSections: globalSectionsProp,
|
|
71
|
+
pageSections,
|
|
72
|
+
navigationLabels,
|
|
73
|
+
accountPageData,
|
|
74
|
+
accountName,
|
|
75
|
+
isRepresentative,
|
|
76
|
+
}: ListCardsPageProps) {
|
|
77
|
+
const { sections: globalSections, settings: globalSettings } =
|
|
78
|
+
globalSectionsProp ?? {}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<PageProvider
|
|
82
|
+
context={{
|
|
83
|
+
globalSettings,
|
|
84
|
+
accountPageData,
|
|
85
|
+
navigationLabels,
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<RenderSections globalSections={globalSections} components={COMPONENTS}>
|
|
89
|
+
<NextSeo noindex nofollow />
|
|
90
|
+
|
|
91
|
+
<Layout
|
|
92
|
+
isRepresentative={isRepresentative}
|
|
93
|
+
accountName={accountName}
|
|
94
|
+
navigationLabels={navigationLabels}
|
|
95
|
+
>
|
|
96
|
+
<RenderSectionsBase
|
|
97
|
+
sections={pageSections}
|
|
98
|
+
components={ACCOUNT_COMPONENTS}
|
|
99
|
+
/>
|
|
100
|
+
</Layout>
|
|
101
|
+
</RenderSections>
|
|
102
|
+
</PageProvider>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const getServerSidePropsBase: GetServerSideProps<
|
|
107
|
+
ListCardsPageProps,
|
|
108
|
+
Record<string, string>,
|
|
109
|
+
Locator
|
|
110
|
+
> = async (context) => {
|
|
111
|
+
const isRepresentative = getIsRepresentative({
|
|
112
|
+
headers: context.req.headers as Record<string, string>,
|
|
113
|
+
account: storeConfig.api.storeId,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const { hasOrgAssociation, hasCustomerId } = getB2BSessionClaims({
|
|
117
|
+
headers: context.req.headers as Record<string, string>,
|
|
118
|
+
account: storeConfig.api.storeId,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const contentContext = {
|
|
122
|
+
previewData: context.previewData,
|
|
123
|
+
locale: context.locale,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { isFaststoreMyAccountEnabled, redirect } = getMyAccountRedirect({
|
|
127
|
+
query: context.query,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (!isFaststoreMyAccountEnabled) {
|
|
131
|
+
return { redirect }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const [
|
|
135
|
+
globalSectionsPromise,
|
|
136
|
+
globalSectionsHeaderPromise,
|
|
137
|
+
globalSectionsFooterPromise,
|
|
138
|
+
] = getGlobalSectionsData(contentContext)
|
|
139
|
+
|
|
140
|
+
const [
|
|
141
|
+
pageContent,
|
|
142
|
+
listCards,
|
|
143
|
+
globalSections,
|
|
144
|
+
globalSectionsHeader,
|
|
145
|
+
globalSectionsFooter,
|
|
146
|
+
] = await Promise.all([
|
|
147
|
+
fetchMyAccountPageContent(
|
|
148
|
+
'myAccountCards',
|
|
149
|
+
contentContext,
|
|
150
|
+
'/pvt/account/cards'
|
|
151
|
+
),
|
|
152
|
+
execute<ServerListCardsQueryQueryVariables, ServerListCardsQueryQuery>(
|
|
153
|
+
{
|
|
154
|
+
variables: {},
|
|
155
|
+
operation: query,
|
|
156
|
+
},
|
|
157
|
+
{ headers: { ...context.req.headers } }
|
|
158
|
+
),
|
|
159
|
+
globalSectionsPromise,
|
|
160
|
+
globalSectionsHeaderPromise,
|
|
161
|
+
globalSectionsFooterPromise,
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
const hasError = !!listCards.errors
|
|
165
|
+
|
|
166
|
+
if (hasError) {
|
|
167
|
+
console.error(...listCards.errors)
|
|
168
|
+
|
|
169
|
+
const statusCode = extractStatusFromError(listCards.errors[0])
|
|
170
|
+
|
|
171
|
+
// Auth failures still redirect (consistent with Orders); anything else
|
|
172
|
+
// (e.g. the Saved-cards service being down) renders the page's own error
|
|
173
|
+
// state instead of a hard redirect (spec US-5) — the buyer stays in My
|
|
174
|
+
// Account and can retry.
|
|
175
|
+
if (statusCode === 403 || statusCode === 401) {
|
|
176
|
+
return {
|
|
177
|
+
redirect: {
|
|
178
|
+
destination: `/pvt/account/403?from=${encodeURIComponent('/pvt/account/cards')}`,
|
|
179
|
+
permanent: false,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// `useAdHocCard` does not gate access to the Cards route (spec
|
|
186
|
+
// my-account-cards-gating-plan, the rectified model) — it, together with
|
|
187
|
+
// `customerId`, gates visibility of the Personal tab only. The route always
|
|
188
|
+
// stays reachable and the sidebar entry always stays visible; there is no
|
|
189
|
+
// redirect and no menu hiding. Default to allowed so a query failure renders
|
|
190
|
+
// the page's own error state instead of hiding the Personal tab.
|
|
191
|
+
const hasAdHocCardAccess = listCards.data?.hasAdHocCardAccess ?? true
|
|
192
|
+
const canViewPersonalCards = hasCustomerId && hasAdHocCardAccess
|
|
193
|
+
|
|
194
|
+
const { pageSections, navigationData } = extractAccountNavigationData(
|
|
195
|
+
pageContent.sections
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const globalSectionsResult = injectGlobalSections({
|
|
199
|
+
globalSections,
|
|
200
|
+
globalSectionsHeader,
|
|
201
|
+
globalSectionsFooter,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// The Saved-cards service returns personal and shared cards merged into one
|
|
205
|
+
// list, each tagged `origin` (spec my-account-cards-shared-listing-unblock).
|
|
206
|
+
// Partition here rather than de-duplicating: a card present under both
|
|
207
|
+
// origins is intentionally rendered in both tabs.
|
|
208
|
+
const allCards = hasError ? [] : (listCards.data?.listCreditCards?.list ?? [])
|
|
209
|
+
// Mirrors MyAccountListCards.tsx's `sharedOnly` exactly — that's the only
|
|
210
|
+
// combination where the UI actually hides the Personal tab (spec
|
|
211
|
+
// my-account-cards-gating-plan, O1). Stripping personalCards any time
|
|
212
|
+
// `canViewPersonalCards` is false would also empty it for the two
|
|
213
|
+
// "kept as-is" combinations (no `unitId`), which still render the tab and,
|
|
214
|
+
// per FR-7, must never be subject to this check at all. Drop the data from
|
|
215
|
+
// the SSR payload only when the chrome that would show it is gone too — an
|
|
216
|
+
// always-false strip ships nothing extra, but a too-broad one silently
|
|
217
|
+
// hides real cards from buyers this gate was never meant to touch.
|
|
218
|
+
const sharedOnly = !canViewPersonalCards && hasOrgAssociation
|
|
219
|
+
const personalCards = sharedOnly
|
|
220
|
+
? []
|
|
221
|
+
: allCards.filter((card) => card.origin === 'personal')
|
|
222
|
+
const sharedCards = allCards.filter((card) => card.origin === 'shared')
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
props: {
|
|
226
|
+
globalSections: globalSectionsResult,
|
|
227
|
+
accountName: listCards.data?.accountProfile.name ?? '',
|
|
228
|
+
navigationLabels: navigationData as AccountNavigationLabels,
|
|
229
|
+
accountPageData: {
|
|
230
|
+
personalCards,
|
|
231
|
+
sharedCards,
|
|
232
|
+
hasOrgAssociation,
|
|
233
|
+
canViewPersonalCards,
|
|
234
|
+
hasError,
|
|
235
|
+
},
|
|
236
|
+
pageSections,
|
|
237
|
+
isRepresentative,
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const getServerSideProps = withLocaleValidationSSR(
|
|
243
|
+
getServerSidePropsBase
|
|
244
|
+
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ServerListCardsQueryQuery,
|
|
2
3
|
ServerListOrdersQueryQuery,
|
|
3
4
|
ServerOrderDetailsQueryQuery,
|
|
4
5
|
ServerProfileQueryQuery,
|
|
@@ -27,6 +28,20 @@ export type AccountOrdersListPageData = {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export type AccountCardsPageData = {
|
|
32
|
+
// Both partitioned from the same listCreditCards response by `origin`
|
|
33
|
+
// (spec my-account-cards-shared-listing-unblock) — one query, no dedup.
|
|
34
|
+
personalCards: NonNullable<
|
|
35
|
+
ServerListCardsQueryQuery['listCreditCards']
|
|
36
|
+
>['list']
|
|
37
|
+
sharedCards: NonNullable<ServerListCardsQueryQuery['listCreditCards']>['list']
|
|
38
|
+
hasOrgAssociation: boolean
|
|
39
|
+
// Gates the Personal tab (spec my-account-cards-gating-plan) — independent
|
|
40
|
+
// of route access, which is never gated.
|
|
41
|
+
canViewPersonalCards: boolean
|
|
42
|
+
hasError: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
export type AccountOrderDetailsPageData = {
|
|
31
46
|
order: ServerOrderDetailsQueryQuery['userOrder']
|
|
32
47
|
orderStatusLabels?: OrderStatusCmsLabels
|
|
@@ -53,6 +68,7 @@ export type AccountPageData =
|
|
|
53
68
|
| AccountOrderDetailsPageData
|
|
54
69
|
| AccountSecurityPageData
|
|
55
70
|
| AccountUserDetailsPageData
|
|
71
|
+
| AccountCardsPageData
|
|
56
72
|
| Record<string, never>
|
|
57
73
|
|
|
58
74
|
export interface AccountPageContext extends PageGlobalContext {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { parse } from 'cookie'
|
|
2
|
+
import { parseJwt } from 'src/utils/getCookie'
|
|
3
|
+
|
|
4
|
+
type GetB2BSessionClaimsParams = {
|
|
5
|
+
headers?: Record<string, string>
|
|
6
|
+
account: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type B2BSessionClaims = {
|
|
10
|
+
/**
|
|
11
|
+
* Whether the buyer is associated with a Unit/Contract. Per the Personal
|
|
12
|
+
* Cards PRD (REQ-2), this is detected by the presence of a `unitId` claim in
|
|
13
|
+
* the VTEX ID JWT — the same cookie `getIsRepresentative` already decodes.
|
|
14
|
+
* Gates the Shared tab (FR-5).
|
|
15
|
+
*
|
|
16
|
+
* Note this is the only part of the gate that lives in the token. The
|
|
17
|
+
* `useAdHocCard` permission is a License Manager resource key resolved from
|
|
18
|
+
* the user's roles, so it is fetched server-side via the
|
|
19
|
+
* `hasAdHocCardAccess` GraphQL query — not read from here.
|
|
20
|
+
*/
|
|
21
|
+
hasOrgAssociation: boolean
|
|
22
|
+
/**
|
|
23
|
+
* Whether the buyer has an individual `customerId` claim, distinct from the
|
|
24
|
+
* `unitId`/contract association. Combined with `hasAdHocCardAccess`, gates
|
|
25
|
+
* the Personal tab (spec my-account-cards-gating-plan, the rectified model —
|
|
26
|
+
* `useAdHocCard` alone doesn't gate the whole Cards route, only Personal-tab
|
|
27
|
+
* visibility).
|
|
28
|
+
*/
|
|
29
|
+
hasCustomerId: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getB2BSessionClaims({
|
|
33
|
+
headers,
|
|
34
|
+
account,
|
|
35
|
+
}: GetB2BSessionClaimsParams): B2BSessionClaims {
|
|
36
|
+
const authCookie = parse(headers?.cookie ?? '')?.[
|
|
37
|
+
'VtexIdclientAutCookie_' + account
|
|
38
|
+
]
|
|
39
|
+
const jwt = parseJwt(authCookie)
|
|
40
|
+
|
|
41
|
+
return { hasOrgAssociation: !!jwt?.unitId, hasCustomerId: !!jwt?.customerId }
|
|
42
|
+
}
|
|
@@ -21,6 +21,7 @@ export type AccountNavigationLabels = Partial<{
|
|
|
21
21
|
switchLabel: string
|
|
22
22
|
companyLabel: string
|
|
23
23
|
contractLabel: string
|
|
24
|
+
cardsLabel: string
|
|
24
25
|
}>
|
|
25
26
|
|
|
26
27
|
interface GetMyAccountRouteParams {
|
|
@@ -33,6 +34,7 @@ export const PROFILE_ROUTE = '/pvt/account/profile'
|
|
|
33
34
|
export const ORDERS_ROUTE = '/pvt/account/orders'
|
|
34
35
|
export const USER_DETAILS_ROUTE = '/pvt/account/user-details'
|
|
35
36
|
export const SECURITY_ROUTE = '/pvt/account/security'
|
|
37
|
+
export const CARDS_ROUTE = '/pvt/account/cards'
|
|
36
38
|
export const QUOTES_ROUTE = '/pvt/account/quotes'
|
|
37
39
|
|
|
38
40
|
export const ROUTES_ONLY_FOR_B2B_MEMBERS = [QUOTES_ROUTE]
|
|
@@ -42,6 +44,7 @@ const ROUTE_LABEL_KEYS: Record<string, keyof AccountNavigationLabels> = {
|
|
|
42
44
|
[ORDERS_ROUTE]: 'ordersLabel',
|
|
43
45
|
[USER_DETAILS_ROUTE]: 'userDetailsLabel',
|
|
44
46
|
[SECURITY_ROUTE]: 'securityLabel',
|
|
47
|
+
[CARDS_ROUTE]: 'cardsLabel',
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
// This is the default route list for My Account, we should add then as the feature is implemented
|
|
@@ -54,6 +57,10 @@ const DEFAULT_ROUTES: Route[] = [
|
|
|
54
57
|
title: 'Orders',
|
|
55
58
|
route: ORDERS_ROUTE,
|
|
56
59
|
},
|
|
60
|
+
{
|
|
61
|
+
title: 'Cards',
|
|
62
|
+
route: CARDS_ROUTE,
|
|
63
|
+
},
|
|
57
64
|
{
|
|
58
65
|
title: 'Quotes',
|
|
59
66
|
route: QUOTES_ROUTE,
|
|
@@ -5,6 +5,7 @@ export type NativeMyAccountContentType =
|
|
|
5
5
|
| 'myAccountUserDetails'
|
|
6
6
|
| 'myAccountSecurity'
|
|
7
7
|
| 'myAccountUnauthorized'
|
|
8
|
+
| 'myAccountCards'
|
|
8
9
|
|
|
9
10
|
/** Store content-types are opaque strings; native literals keep autocompletion. */
|
|
10
11
|
export type MyAccountContentType = NativeMyAccountContentType | (string & {})
|
|
@@ -18,6 +19,7 @@ export type DefaultMyAccountSection = {
|
|
|
18
19
|
const DEFAULT_SECTION_KEYS: Record<NativeMyAccountContentType, string[]> = {
|
|
19
20
|
myAccountProfile: ['AccountNavigation', 'AccountProfile'],
|
|
20
21
|
myAccountOrders: ['AccountNavigation', 'AccountOrdersList'],
|
|
22
|
+
myAccountCards: ['AccountNavigation', 'AccountListCards'],
|
|
21
23
|
myAccountOrderDetails: [
|
|
22
24
|
'AccountNavigation',
|
|
23
25
|
'AccountOrderDetails',
|
package/src/utils/getCookie.ts
CHANGED
|
@@ -27,7 +27,14 @@ export function parseJwt(token: string) {
|
|
|
27
27
|
if (!token) {
|
|
28
28
|
return null
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
|
|
33
|
+
} catch {
|
|
34
|
+
// Malformed structure/base64/JSON — treat the same as no token, instead
|
|
35
|
+
// of throwing and 500ing the caller's request.
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export function getJWTAutCookie({ headers, account }: Params) {
|