@graphcommerce/magento-product-downloadable 9.0.4-canary.8 → 9.1.0-canary.15
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/CHANGELOG.md +23 -0
- package/README.md +1 -0
- package/components/DownloadableAccountMenuItem/DownloadableAccountMenuItem.graphql +11 -0
- package/components/DownloadableAccountMenuItem/DownloadableAccountMenuItem.tsx +32 -0
- package/components/DownloadableCartItemOptions/DownloadableCartItemOptions.tsx +39 -0
- package/components/DownloadableProductOptions/DownloadableProductOptions.tsx +43 -11
- package/components/DownloadsPage/DownloadsPage.graphql +11 -0
- package/components/DownloadsPage/DownloadsPage.tsx +134 -0
- package/copy/pages/account/downloads.tsx +26 -0
- package/copy/pages/downloadable/download/link/id/[id].tsx +104 -0
- package/copy/pages/downloadable/download/linkSample/link_id/[id].tsx +107 -0
- package/copy/pages/downloadable/download/sample/sample_id/[id].tsx +107 -0
- package/index.ts +5 -3
- package/package.json +13 -10
- package/plugins/DownloadableAccountMenuItem.tsx +19 -0
- package/plugins/DownloadableCartItemActionCard.tsx +30 -0
- package/plugins/DownloadableProductPagePrice.tsx +69 -0
- package/plugins/Downloadable_cartItemToCartItemInput.ts +36 -0
- package/DownloadableProductPage.graphql +0 -3
- package/ProductPageDownloadableQueryFragment.graphql +0 -22
- package/graphql/GetDownloadableTypeProduct.graphql +0 -9
- /package/{ProductListItemDownloadable.graphql → components/ProductListItemDownloadable/ProductListItemDownloadable.graphql} +0 -0
- /package/{ProductListItemDownloadable.tsx → components/ProductListItemDownloadable/ProductListItemDownloadable.tsx} +0 -0
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,28 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 9.1.0-canary.15
|
4
|
+
|
5
|
+
### Minor Changes
|
6
|
+
|
7
|
+
- [#2493](https://github.com/graphcommerce-org/graphcommerce/pull/2493) [`01ba1a7`](https://github.com/graphcommerce-org/graphcommerce/commit/01ba1a73c86778ea9b34059ec092723b4a56b92b) - Improved Downloadable products functionality:
|
8
|
+
|
9
|
+
- Account Dashboard Link and Download Page
|
10
|
+
- Download samples / linkSamples and links from the backend.
|
11
|
+
- CartItem edit functionality for the DownloadableCartItem
|
12
|
+
- Dynamic ProductPagePrice for downloadable options ([@paales](https://github.com/paales))
|
13
|
+
|
14
|
+
## 9.0.4-canary.14
|
15
|
+
|
16
|
+
## 9.0.4-canary.13
|
17
|
+
|
18
|
+
## 9.0.4-canary.12
|
19
|
+
|
20
|
+
## 9.0.4-canary.11
|
21
|
+
|
22
|
+
## 9.0.4-canary.10
|
23
|
+
|
24
|
+
## 9.0.4-canary.9
|
25
|
+
|
3
26
|
## 9.0.4-canary.8
|
4
27
|
|
5
28
|
## 9.0.4-canary.7
|
package/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Magento Downloadable Products
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import {
|
2
|
+
AccountDashboardDocument,
|
3
|
+
AccountMenuItem,
|
4
|
+
useCustomerQuery,
|
5
|
+
type AccountMenuItemProps,
|
6
|
+
} from '@graphcommerce/magento-customer'
|
7
|
+
import { filterNonNullableKeys, iconDownload } from '@graphcommerce/next-ui'
|
8
|
+
import { Trans } from '@lingui/macro'
|
9
|
+
import type { SetOptional } from 'type-fest'
|
10
|
+
|
11
|
+
type StoreCreditAccountMenuItemProps = SetOptional<
|
12
|
+
Omit<AccountMenuItemProps, 'href'>,
|
13
|
+
'iconSrc' | 'title'
|
14
|
+
>
|
15
|
+
|
16
|
+
export function DownloadableAccountMenuItem(props: StoreCreditAccountMenuItemProps) {
|
17
|
+
const dashboard = useCustomerQuery(AccountDashboardDocument, { fetchPolicy: 'cache-only' })
|
18
|
+
const downloadable = dashboard.data?.customerDownloadableProducts
|
19
|
+
|
20
|
+
const items = filterNonNullableKeys(downloadable?.items)
|
21
|
+
const count = items.length
|
22
|
+
|
23
|
+
return (
|
24
|
+
<AccountMenuItem
|
25
|
+
href='/account/downloads'
|
26
|
+
iconSrc={iconDownload}
|
27
|
+
title={<Trans id='Downloads'>Downloads</Trans>}
|
28
|
+
subtitle={<Trans>You have {count} downloads available.</Trans>}
|
29
|
+
{...props}
|
30
|
+
/>
|
31
|
+
)
|
32
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import {
|
2
|
+
SelectedCustomizableOptions,
|
3
|
+
type CartItemActionCardProps,
|
4
|
+
} from '@graphcommerce/magento-cart-items'
|
5
|
+
import { Money } from '@graphcommerce/magento-store'
|
6
|
+
import { filterNonNullableKeys } from '@graphcommerce/next-ui'
|
7
|
+
import { Box } from '@mui/material'
|
8
|
+
import React from 'react'
|
9
|
+
|
10
|
+
export function DownloadableCartItemOptions(props: CartItemActionCardProps) {
|
11
|
+
const { cartItem } = props
|
12
|
+
|
13
|
+
if (cartItem.__typename !== 'DownloadableCartItem') return null
|
14
|
+
|
15
|
+
const links = filterNonNullableKeys(cartItem.links, ['title', 'price'])
|
16
|
+
|
17
|
+
return (
|
18
|
+
<>
|
19
|
+
<Box
|
20
|
+
sx={(theme) => ({
|
21
|
+
display: 'grid',
|
22
|
+
gridTemplateColumns: 'auto auto',
|
23
|
+
gap: theme.spacings.xs,
|
24
|
+
})}
|
25
|
+
>
|
26
|
+
{links.map((link) => (
|
27
|
+
<React.Fragment key={link.uid}>
|
28
|
+
<span>{link.title}</span>
|
29
|
+
<Box>
|
30
|
+
<Money value={link.price} currency={cartItem.prices?.price.currency} />
|
31
|
+
</Box>
|
32
|
+
</React.Fragment>
|
33
|
+
))}
|
34
|
+
</Box>
|
35
|
+
|
36
|
+
<SelectedCustomizableOptions {...cartItem} />
|
37
|
+
</>
|
38
|
+
)
|
39
|
+
}
|
@@ -3,7 +3,13 @@ import type { AddToCartItemSelector } from '@graphcommerce/magento-product'
|
|
3
3
|
import { useFormAddProductsToCart } from '@graphcommerce/magento-product'
|
4
4
|
import { Money } from '@graphcommerce/magento-store'
|
5
5
|
import type { ActionCardProps } from '@graphcommerce/next-ui'
|
6
|
-
import {
|
6
|
+
import {
|
7
|
+
ActionCard,
|
8
|
+
filterNonNullableKeys,
|
9
|
+
ListFormat,
|
10
|
+
SectionHeader,
|
11
|
+
} from '@graphcommerce/next-ui'
|
12
|
+
import { Box, Link } from '@mui/material'
|
7
13
|
import { useMemo } from 'react'
|
8
14
|
import type { DownloadableProductOptionsFragment } from './DownloadableProductOptions.gql'
|
9
15
|
|
@@ -15,28 +21,54 @@ export function DownloadableProductOptions(props: DownloadableProductOptionsProp
|
|
15
21
|
const { product, index = 0 } = props
|
16
22
|
const { control } = useFormAddProductsToCart()
|
17
23
|
|
18
|
-
const
|
24
|
+
const options = useMemo(
|
19
25
|
() =>
|
20
26
|
filterNonNullableKeys(product.downloadable_product_links, ['title']).map((item) => {
|
21
27
|
const newItem: ActionCardProps = {
|
22
28
|
value: item.uid,
|
23
29
|
title: item.title,
|
24
30
|
price: <Money value={item.price} />,
|
31
|
+
details: item.sample_url ? <Link href={item.sample_url}>View Sample</Link> : null,
|
25
32
|
}
|
26
33
|
return newItem
|
27
34
|
}),
|
28
35
|
[product.downloadable_product_links],
|
29
36
|
)
|
30
37
|
|
38
|
+
const samples = filterNonNullableKeys(product.downloadable_product_samples, [
|
39
|
+
'sort_order',
|
40
|
+
'title',
|
41
|
+
'sample_url',
|
42
|
+
]).sort((a, b) => a.sort_order - b.sort_order)
|
43
|
+
|
31
44
|
return (
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
45
|
+
<>
|
46
|
+
<Box>
|
47
|
+
<SectionHeader labelLeft='Downloadable Option' />
|
48
|
+
<ActionCardListForm
|
49
|
+
multiple
|
50
|
+
size='medium'
|
51
|
+
required
|
52
|
+
errorMessage='Please select an option'
|
53
|
+
control={control}
|
54
|
+
name={`cartItems.${index}.selected_options`}
|
55
|
+
render={ActionCard}
|
56
|
+
items={options}
|
57
|
+
/>
|
58
|
+
</Box>
|
59
|
+
|
60
|
+
{samples.length > 0 ? (
|
61
|
+
<Box>
|
62
|
+
<SectionHeader labelLeft='Samples' />
|
63
|
+
<ListFormat>
|
64
|
+
{samples.map((sample) => (
|
65
|
+
<Link key={sample.sample_url} href={sample.sample_url}>
|
66
|
+
{sample.title}
|
67
|
+
</Link>
|
68
|
+
))}
|
69
|
+
</ListFormat>
|
70
|
+
</Box>
|
71
|
+
) : null}
|
72
|
+
</>
|
41
73
|
)
|
42
74
|
}
|
@@ -0,0 +1,134 @@
|
|
1
|
+
import type { ApolloClient, NormalizedCacheObject } from '@graphcommerce/graphql'
|
2
|
+
import {
|
3
|
+
getCustomerAccountIsDisabled,
|
4
|
+
useCustomerQuery,
|
5
|
+
useCustomerSession,
|
6
|
+
WaitForCustomer,
|
7
|
+
} from '@graphcommerce/magento-customer'
|
8
|
+
import { PageMeta, StoreConfigDocument } from '@graphcommerce/magento-store'
|
9
|
+
import {
|
10
|
+
Button,
|
11
|
+
DateFormat,
|
12
|
+
filterNonNullableKeys,
|
13
|
+
iconCreditCard,
|
14
|
+
iconDownload,
|
15
|
+
LayoutOverlayHeader,
|
16
|
+
LayoutTitle,
|
17
|
+
RelativeToTimeFormat,
|
18
|
+
SectionContainer,
|
19
|
+
type GetStaticProps,
|
20
|
+
} from '@graphcommerce/next-ui'
|
21
|
+
import { i18n } from '@lingui/core'
|
22
|
+
import { t, Trans } from '@lingui/macro'
|
23
|
+
import { Box, Container, Link, Typography } from '@mui/material'
|
24
|
+
import React from 'react'
|
25
|
+
import { DownloadsPageDocument } from './DownloadsPage.gql'
|
26
|
+
|
27
|
+
export type DownloadsPageProps = Record<string, unknown>
|
28
|
+
type DownloadsGetStaticProps = GetStaticProps<Record<string, unknown>, DownloadsPageProps>
|
29
|
+
|
30
|
+
export function DownloadsPage() {
|
31
|
+
const dashboard = useCustomerQuery(DownloadsPageDocument, {
|
32
|
+
fetchPolicy: 'cache-and-network',
|
33
|
+
})
|
34
|
+
|
35
|
+
const downloads = filterNonNullableKeys(dashboard.data?.customerDownloadableProducts?.items)
|
36
|
+
|
37
|
+
const session = useCustomerSession()
|
38
|
+
|
39
|
+
return (
|
40
|
+
<>
|
41
|
+
<LayoutOverlayHeader>
|
42
|
+
<LayoutTitle size='small' component='span' icon={iconDownload}>
|
43
|
+
<Trans id='Downloads'>Downloads</Trans>
|
44
|
+
</LayoutTitle>
|
45
|
+
</LayoutOverlayHeader>
|
46
|
+
|
47
|
+
<Container maxWidth='md'>
|
48
|
+
<WaitForCustomer waitFor={dashboard}>
|
49
|
+
<PageMeta title={t`Downloads`} metaRobots={['noindex']} />
|
50
|
+
|
51
|
+
<LayoutTitle
|
52
|
+
icon={iconDownload}
|
53
|
+
sx={(theme) => ({ mb: theme.spacings.xs })}
|
54
|
+
gutterBottom={false}
|
55
|
+
>
|
56
|
+
<Trans id='Downloads'>Downloads</Trans>
|
57
|
+
</LayoutTitle>
|
58
|
+
|
59
|
+
<Box>
|
60
|
+
<SectionContainer labelLeft={<Trans id='Balance history'>Balance history</Trans>}>
|
61
|
+
<Box
|
62
|
+
sx={{
|
63
|
+
display: 'grid',
|
64
|
+
gridTemplateColumns: 'repeat(5, auto)',
|
65
|
+
gap: 1,
|
66
|
+
alignItems: 'center',
|
67
|
+
}}
|
68
|
+
>
|
69
|
+
<Typography variant='subtitle1'>
|
70
|
+
<Trans id='Order'>Order</Trans>
|
71
|
+
</Typography>
|
72
|
+
<Typography variant='subtitle1'>
|
73
|
+
<Trans id='Download'>Download</Trans>
|
74
|
+
</Typography>
|
75
|
+
<Typography variant='subtitle1'>
|
76
|
+
<Trans id='Remaining'>Remaining</Trans>
|
77
|
+
</Typography>
|
78
|
+
<Typography variant='subtitle1'>
|
79
|
+
<Trans id='Status'>Status</Trans>
|
80
|
+
</Typography>
|
81
|
+
<Typography variant='subtitle1'>
|
82
|
+
<Trans id='Date'>Date</Trans>
|
83
|
+
</Typography>
|
84
|
+
|
85
|
+
{downloads.map((item, index) => (
|
86
|
+
// eslint-disable-next-line react/no-array-index-key
|
87
|
+
<React.Fragment key={index}>
|
88
|
+
<Box>
|
89
|
+
<Link href={`/account/orders/view?orderNumber=${item.order_increment_id}`}>
|
90
|
+
{item.order_increment_id}
|
91
|
+
</Link>
|
92
|
+
</Box>
|
93
|
+
<Box>
|
94
|
+
{item.status === 'available' ? (
|
95
|
+
<Link href={item.download_url} color='secondary'>
|
96
|
+
Download now
|
97
|
+
</Link>
|
98
|
+
) : (
|
99
|
+
<Typography variant='subtitle1'>Order not completed</Typography>
|
100
|
+
)}
|
101
|
+
</Box>
|
102
|
+
<Box>{item.remaining_downloads}</Box>
|
103
|
+
<Box>{item.status}</Box>
|
104
|
+
<Box>
|
105
|
+
<RelativeToTimeFormat date={item.date} />
|
106
|
+
</Box>
|
107
|
+
</React.Fragment>
|
108
|
+
))}
|
109
|
+
</Box>
|
110
|
+
</SectionContainer>
|
111
|
+
</Box>
|
112
|
+
</WaitForCustomer>
|
113
|
+
</Container>
|
114
|
+
</>
|
115
|
+
)
|
116
|
+
}
|
117
|
+
|
118
|
+
export function createGetStaticProps(
|
119
|
+
client: ApolloClient<NormalizedCacheObject>,
|
120
|
+
): DownloadsGetStaticProps {
|
121
|
+
return async (context) => {
|
122
|
+
if (getCustomerAccountIsDisabled(context.locale)) return { notFound: true }
|
123
|
+
|
124
|
+
const conf = client.query({ query: StoreConfigDocument })
|
125
|
+
|
126
|
+
return {
|
127
|
+
props: {
|
128
|
+
apolloState: await conf.then(() => client.cache.extract()),
|
129
|
+
variantMd: 'bottom',
|
130
|
+
up: { href: '/account', title: i18n._('Account') },
|
131
|
+
},
|
132
|
+
}
|
133
|
+
}
|
134
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
2
|
+
import type { PageOptions } from '@graphcommerce/framer-next-pages'
|
3
|
+
import type { DownloadsPageProps } from '@graphcommerce/magento-product-downloadable'
|
4
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
5
|
+
import { createGetStaticProps, DownloadsPage } from '@graphcommerce/magento-product-downloadable'
|
6
|
+
import type { GetStaticProps, LayoutOverlayProps } from '@graphcommerce/next-ui'
|
7
|
+
import { LayoutOverlay } from '@graphcommerce/next-ui'
|
8
|
+
import { graphqlSharedClient } from '../../lib/graphql/graphqlSsrClient'
|
9
|
+
|
10
|
+
type GetPageStaticProps = GetStaticProps<LayoutOverlayProps>
|
11
|
+
|
12
|
+
export default function Page(props: DownloadsPageProps) {
|
13
|
+
return <DownloadsPage {...props} />
|
14
|
+
}
|
15
|
+
|
16
|
+
const pageOptions: PageOptions<LayoutOverlayProps> = {
|
17
|
+
overlayGroup: 'account',
|
18
|
+
Layout: LayoutOverlay,
|
19
|
+
}
|
20
|
+
|
21
|
+
Page.pageOptions = pageOptions
|
22
|
+
|
23
|
+
export const getStaticProps: GetPageStaticProps = async (context) => {
|
24
|
+
const client = graphqlSharedClient(context)
|
25
|
+
return createGetStaticProps(client)(context)
|
26
|
+
}
|
@@ -0,0 +1,104 @@
|
|
1
|
+
// managed by: graphcommerce
|
2
|
+
// to modify this file, change it to managed by: local
|
3
|
+
|
4
|
+
import type { PageOptions } from '@graphcommerce/framer-next-pages'
|
5
|
+
import { cacheFirst } from '@graphcommerce/graphql'
|
6
|
+
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
7
|
+
import { FullPageMessage, iconError, IconSvg } from '@graphcommerce/next-ui'
|
8
|
+
import type { GetServerSideProps } from 'next'
|
9
|
+
import {
|
10
|
+
LayoutDocument,
|
11
|
+
LayoutNavigation,
|
12
|
+
type LayoutNavigationProps,
|
13
|
+
} from '../../../../../components'
|
14
|
+
import { graphqlSsrClient } from '../../../../../lib/graphql/graphqlSsrClient'
|
15
|
+
|
16
|
+
type ErrorType = 'not-sharable' | 'unknown'
|
17
|
+
|
18
|
+
type Props = { errorType: ErrorType }
|
19
|
+
|
20
|
+
export default function DownloadLinkPage(props: Props) {
|
21
|
+
const { errorType } = props
|
22
|
+
return (
|
23
|
+
<FullPageMessage
|
24
|
+
icon={<IconSvg src={iconError} size='xxl' />}
|
25
|
+
title={<>Error while downloading file</>}
|
26
|
+
>
|
27
|
+
{errorType === 'not-sharable' && (
|
28
|
+
<>
|
29
|
+
The requested file of found, but is not “Sharable” and can therefor not be
|
30
|
+
downloaded.
|
31
|
+
</>
|
32
|
+
)}
|
33
|
+
</FullPageMessage>
|
34
|
+
)
|
35
|
+
}
|
36
|
+
|
37
|
+
DownloadLinkPage.pageOptions = {
|
38
|
+
Layout: LayoutNavigation,
|
39
|
+
} as PageOptions
|
40
|
+
|
41
|
+
export const getServerSideProps: GetServerSideProps<Props & LayoutNavigationProps> = async (
|
42
|
+
context,
|
43
|
+
) => {
|
44
|
+
const { query } = context
|
45
|
+
const id = query?.id
|
46
|
+
|
47
|
+
const staticClient = graphqlSsrClient(context)
|
48
|
+
const conf = await staticClient.query({ query: StoreConfigDocument })
|
49
|
+
|
50
|
+
const baseUrl = conf.data.storeConfig?.secure_base_url
|
51
|
+
if (!baseUrl) return { notFound: true }
|
52
|
+
|
53
|
+
const downloadUrl = new URL(baseUrl)
|
54
|
+
downloadUrl.pathname = `/downloadable/download/link/id/${id}`
|
55
|
+
|
56
|
+
const response = await fetch(downloadUrl, {
|
57
|
+
redirect: 'manual',
|
58
|
+
})
|
59
|
+
|
60
|
+
async function pageResponse(errorType: ErrorType) {
|
61
|
+
const layout = staticClient.query({
|
62
|
+
query: LayoutDocument,
|
63
|
+
fetchPolicy: cacheFirst(staticClient),
|
64
|
+
})
|
65
|
+
return {
|
66
|
+
props: {
|
67
|
+
errorType,
|
68
|
+
...(await layout).data,
|
69
|
+
apolloState: staticClient.cache.extract(),
|
70
|
+
},
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
if (response.status === 302) {
|
75
|
+
const location = response.headers.get('Location')
|
76
|
+
const requestSignIn = location?.includes('customer/account/login')
|
77
|
+
if (requestSignIn) return pageResponse('not-sharable')
|
78
|
+
return pageResponse('unknown')
|
79
|
+
}
|
80
|
+
|
81
|
+
if (response.status !== 200 || !response.body) return { notFound: true }
|
82
|
+
|
83
|
+
context.res.setHeader(
|
84
|
+
'Content-Type',
|
85
|
+
response.headers.get('Content-Type') ?? 'application/octet-stream',
|
86
|
+
)
|
87
|
+
context.res.setHeader(
|
88
|
+
'Content-Disposition',
|
89
|
+
response.headers.get('Content-Disposition') ?? 'attachment',
|
90
|
+
)
|
91
|
+
|
92
|
+
await response.body.pipeTo(
|
93
|
+
new WritableStream<Uint8Array>({
|
94
|
+
write: (chunk) => {
|
95
|
+
context.res.write(chunk)
|
96
|
+
},
|
97
|
+
close() {
|
98
|
+
context.res.end()
|
99
|
+
},
|
100
|
+
}),
|
101
|
+
)
|
102
|
+
|
103
|
+
return pageResponse('unknown')
|
104
|
+
}
|
@@ -0,0 +1,107 @@
|
|
1
|
+
// managed by: graphcommerce
|
2
|
+
// to modify this file, change it to managed by: local
|
3
|
+
|
4
|
+
// managed by: graphcommerce
|
5
|
+
// to modify this file, change it to managed by: local
|
6
|
+
|
7
|
+
import type { PageOptions } from '@graphcommerce/framer-next-pages'
|
8
|
+
import { cacheFirst } from '@graphcommerce/graphql'
|
9
|
+
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
10
|
+
import { FullPageMessage, iconError, IconSvg } from '@graphcommerce/next-ui'
|
11
|
+
import type { GetServerSideProps } from 'next'
|
12
|
+
import {
|
13
|
+
LayoutDocument,
|
14
|
+
LayoutNavigation,
|
15
|
+
type LayoutNavigationProps,
|
16
|
+
} from '../../../../../components'
|
17
|
+
import { graphqlSsrClient } from '../../../../../lib/graphql/graphqlSsrClient'
|
18
|
+
|
19
|
+
type ErrorType = 'not-sharable' | 'unknown'
|
20
|
+
|
21
|
+
type Props = { errorType: ErrorType }
|
22
|
+
|
23
|
+
export default function DownloadLinkPage(props: Props) {
|
24
|
+
const { errorType } = props
|
25
|
+
return (
|
26
|
+
<FullPageMessage
|
27
|
+
icon={<IconSvg src={iconError} size='xxl' />}
|
28
|
+
title={<>Error while downloading file</>}
|
29
|
+
>
|
30
|
+
{errorType === 'not-sharable' && (
|
31
|
+
<>
|
32
|
+
The requested file of found, but is not “Sharable” and can therefor not be
|
33
|
+
downloaded.
|
34
|
+
</>
|
35
|
+
)}
|
36
|
+
</FullPageMessage>
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
DownloadLinkPage.pageOptions = {
|
41
|
+
Layout: LayoutNavigation,
|
42
|
+
} as PageOptions
|
43
|
+
|
44
|
+
export const getServerSideProps: GetServerSideProps<Props & LayoutNavigationProps> = async (
|
45
|
+
context,
|
46
|
+
) => {
|
47
|
+
const { query } = context
|
48
|
+
const id = query?.id
|
49
|
+
|
50
|
+
const staticClient = graphqlSsrClient(context)
|
51
|
+
const conf = await staticClient.query({ query: StoreConfigDocument })
|
52
|
+
|
53
|
+
const baseUrl = conf.data.storeConfig?.secure_base_url
|
54
|
+
if (!baseUrl) return { notFound: true }
|
55
|
+
|
56
|
+
const downloadUrl = new URL(baseUrl)
|
57
|
+
downloadUrl.pathname = `/downloadable/download/linkSample/link_id/${id}`
|
58
|
+
|
59
|
+
const response = await fetch(downloadUrl, {
|
60
|
+
redirect: 'manual',
|
61
|
+
})
|
62
|
+
|
63
|
+
async function pageResponse(errorType: ErrorType) {
|
64
|
+
const layout = staticClient.query({
|
65
|
+
query: LayoutDocument,
|
66
|
+
fetchPolicy: cacheFirst(staticClient),
|
67
|
+
})
|
68
|
+
return {
|
69
|
+
props: {
|
70
|
+
errorType,
|
71
|
+
...(await layout).data,
|
72
|
+
apolloState: staticClient.cache.extract(),
|
73
|
+
},
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
if (response.status === 302) {
|
78
|
+
const location = response.headers.get('Location')
|
79
|
+
const requestSignIn = location?.includes('customer/account/login')
|
80
|
+
if (requestSignIn) return pageResponse('not-sharable')
|
81
|
+
return pageResponse('unknown')
|
82
|
+
}
|
83
|
+
|
84
|
+
if (response.status !== 200 || !response.body) return { notFound: true }
|
85
|
+
|
86
|
+
context.res.setHeader(
|
87
|
+
'Content-Type',
|
88
|
+
response.headers.get('Content-Type') ?? 'application/octet-stream',
|
89
|
+
)
|
90
|
+
context.res.setHeader(
|
91
|
+
'Content-Disposition',
|
92
|
+
response.headers.get('Content-Disposition') ?? 'attachment',
|
93
|
+
)
|
94
|
+
|
95
|
+
await response.body.pipeTo(
|
96
|
+
new WritableStream<Uint8Array>({
|
97
|
+
write: (chunk) => {
|
98
|
+
context.res.write(chunk)
|
99
|
+
},
|
100
|
+
close() {
|
101
|
+
context.res.end()
|
102
|
+
},
|
103
|
+
}),
|
104
|
+
)
|
105
|
+
|
106
|
+
return pageResponse('unknown')
|
107
|
+
}
|
@@ -0,0 +1,107 @@
|
|
1
|
+
// managed by: graphcommerce
|
2
|
+
// to modify this file, change it to managed by: local
|
3
|
+
|
4
|
+
// managed by: graphcommerce
|
5
|
+
// to modify this file, change it to managed by: local
|
6
|
+
|
7
|
+
import type { PageOptions } from '@graphcommerce/framer-next-pages'
|
8
|
+
import { cacheFirst } from '@graphcommerce/graphql'
|
9
|
+
import { StoreConfigDocument } from '@graphcommerce/magento-store'
|
10
|
+
import { FullPageMessage, iconError, IconSvg } from '@graphcommerce/next-ui'
|
11
|
+
import type { GetServerSideProps } from 'next'
|
12
|
+
import {
|
13
|
+
LayoutDocument,
|
14
|
+
LayoutNavigation,
|
15
|
+
type LayoutNavigationProps,
|
16
|
+
} from '../../../../../components'
|
17
|
+
import { graphqlSsrClient } from '../../../../../lib/graphql/graphqlSsrClient'
|
18
|
+
|
19
|
+
type ErrorType = 'not-sharable' | 'unknown'
|
20
|
+
|
21
|
+
type Props = { errorType: ErrorType }
|
22
|
+
|
23
|
+
export default function DownloadLinkPage(props: Props) {
|
24
|
+
const { errorType } = props
|
25
|
+
return (
|
26
|
+
<FullPageMessage
|
27
|
+
icon={<IconSvg src={iconError} size='xxl' />}
|
28
|
+
title={<>Error while downloading file</>}
|
29
|
+
>
|
30
|
+
{errorType === 'not-sharable' && (
|
31
|
+
<>
|
32
|
+
The requested file of found, but is not “Sharable” and can therefor not be
|
33
|
+
downloaded.
|
34
|
+
</>
|
35
|
+
)}
|
36
|
+
</FullPageMessage>
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
DownloadLinkPage.pageOptions = {
|
41
|
+
Layout: LayoutNavigation,
|
42
|
+
} as PageOptions
|
43
|
+
|
44
|
+
export const getServerSideProps: GetServerSideProps<Props & LayoutNavigationProps> = async (
|
45
|
+
context,
|
46
|
+
) => {
|
47
|
+
const { query } = context
|
48
|
+
const id = query?.id
|
49
|
+
|
50
|
+
const staticClient = graphqlSsrClient(context)
|
51
|
+
const conf = await staticClient.query({ query: StoreConfigDocument })
|
52
|
+
|
53
|
+
const baseUrl = conf.data.storeConfig?.secure_base_url
|
54
|
+
if (!baseUrl) return { notFound: true }
|
55
|
+
|
56
|
+
const downloadUrl = new URL(baseUrl)
|
57
|
+
downloadUrl.pathname = `/downloadable/download/sample/sample_id/${id}`
|
58
|
+
|
59
|
+
const response = await fetch(downloadUrl, {
|
60
|
+
redirect: 'manual',
|
61
|
+
})
|
62
|
+
|
63
|
+
async function pageResponse(errorType: ErrorType) {
|
64
|
+
const layout = staticClient.query({
|
65
|
+
query: LayoutDocument,
|
66
|
+
fetchPolicy: cacheFirst(staticClient),
|
67
|
+
})
|
68
|
+
return {
|
69
|
+
props: {
|
70
|
+
errorType,
|
71
|
+
...(await layout).data,
|
72
|
+
apolloState: staticClient.cache.extract(),
|
73
|
+
},
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
if (response.status === 302) {
|
78
|
+
const location = response.headers.get('Location')
|
79
|
+
const requestSignIn = location?.includes('customer/account/login')
|
80
|
+
if (requestSignIn) return pageResponse('not-sharable')
|
81
|
+
return pageResponse('unknown')
|
82
|
+
}
|
83
|
+
|
84
|
+
if (response.status !== 200 || !response.body) return { notFound: true }
|
85
|
+
|
86
|
+
context.res.setHeader(
|
87
|
+
'Content-Type',
|
88
|
+
response.headers.get('Content-Type') ?? 'application/octet-stream',
|
89
|
+
)
|
90
|
+
context.res.setHeader(
|
91
|
+
'Content-Disposition',
|
92
|
+
response.headers.get('Content-Disposition') ?? 'attachment',
|
93
|
+
)
|
94
|
+
|
95
|
+
await response.body.pipeTo(
|
96
|
+
new WritableStream<Uint8Array>({
|
97
|
+
write: (chunk) => {
|
98
|
+
context.res.write(chunk)
|
99
|
+
},
|
100
|
+
close() {
|
101
|
+
context.res.end()
|
102
|
+
},
|
103
|
+
}),
|
104
|
+
)
|
105
|
+
|
106
|
+
return pageResponse('unknown')
|
107
|
+
}
|
package/index.ts
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
export * from './
|
2
|
-
export * from './ProductListItemDownloadable'
|
3
|
-
export * from './ProductListItemDownloadable.gql'
|
1
|
+
export * from './components/ProductListItemDownloadable/ProductListItemDownloadable'
|
2
|
+
export * from './components/ProductListItemDownloadable/ProductListItemDownloadable.gql'
|
4
3
|
export * from './components/DownloadableProductOptions'
|
4
|
+
export * from './components/DownloadableAccountMenuItem/DownloadableAccountMenuItem'
|
5
|
+
export * from './components/DownloadableAccountMenuItem/DownloadableAccountMenuItem.gql'
|
6
|
+
export * from './components/DownloadsPage/DownloadsPage'
|
package/package.json
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
"name": "@graphcommerce/magento-product-downloadable",
|
3
3
|
"homepage": "https://www.graphcommerce.org/",
|
4
4
|
"repository": "github:graphcommerce-org/graphcommerce",
|
5
|
-
"version": "9.0
|
5
|
+
"version": "9.1.0-canary.15",
|
6
6
|
"sideEffects": false,
|
7
7
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
8
8
|
"eslintConfig": {
|
@@ -12,18 +12,21 @@
|
|
12
12
|
}
|
13
13
|
},
|
14
14
|
"peerDependencies": {
|
15
|
-
"@graphcommerce/ecommerce-ui": "^9.0
|
16
|
-
"@graphcommerce/eslint-config-pwa": "^9.0
|
17
|
-
"@graphcommerce/graphql": "^9.0
|
18
|
-
"@graphcommerce/magento-cart": "^9.0
|
19
|
-
"@graphcommerce/magento-
|
20
|
-
"@graphcommerce/magento-
|
21
|
-
"@graphcommerce/
|
22
|
-
"@graphcommerce/
|
23
|
-
"@graphcommerce/
|
15
|
+
"@graphcommerce/ecommerce-ui": "^9.1.0-canary.15",
|
16
|
+
"@graphcommerce/eslint-config-pwa": "^9.1.0-canary.15",
|
17
|
+
"@graphcommerce/graphql": "^9.1.0-canary.15",
|
18
|
+
"@graphcommerce/magento-cart": "^9.1.0-canary.15",
|
19
|
+
"@graphcommerce/magento-cart-items": "^9.1.0-canary.15",
|
20
|
+
"@graphcommerce/magento-customer": "^9.1.0-canary.15",
|
21
|
+
"@graphcommerce/magento-product": "^9.1.0-canary.15",
|
22
|
+
"@graphcommerce/magento-store": "^9.1.0-canary.15",
|
23
|
+
"@graphcommerce/next-ui": "^9.1.0-canary.15",
|
24
|
+
"@graphcommerce/prettier-config-pwa": "^9.1.0-canary.15",
|
25
|
+
"@graphcommerce/typescript-config-pwa": "^9.1.0-canary.15",
|
24
26
|
"@lingui/core": "^4.2.1",
|
25
27
|
"@lingui/macro": "^4.2.1",
|
26
28
|
"@lingui/react": "^4.2.1",
|
29
|
+
"@mui/material": "^5.10.16",
|
27
30
|
"next": "*",
|
28
31
|
"react": "^18.2.0",
|
29
32
|
"react-dom": "^18.2.0"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import type { AccountMenuItemProps } from '@graphcommerce/magento-customer'
|
2
|
+
import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
|
3
|
+
import { DownloadableAccountMenuItem } from '../components/DownloadableAccountMenuItem/DownloadableAccountMenuItem'
|
4
|
+
|
5
|
+
export const config: PluginConfig = {
|
6
|
+
module: '@graphcommerce/magento-customer',
|
7
|
+
type: 'component',
|
8
|
+
}
|
9
|
+
|
10
|
+
export function AccountMenuItem(props: PluginProps<AccountMenuItemProps>) {
|
11
|
+
const { Prev, href, ...rest } = props
|
12
|
+
|
13
|
+
return (
|
14
|
+
<>
|
15
|
+
<Prev {...rest} href={href} />
|
16
|
+
{href === '/account/orders' && <DownloadableAccountMenuItem />}
|
17
|
+
</>
|
18
|
+
)
|
19
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import {
|
2
|
+
SelectedCustomizableOptions,
|
3
|
+
type CartItemActionCardProps,
|
4
|
+
} from '@graphcommerce/magento-cart-items'
|
5
|
+
import { Money } from '@graphcommerce/magento-store'
|
6
|
+
import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
|
7
|
+
import { filterNonNullableKeys, isTypename } from '@graphcommerce/next-ui'
|
8
|
+
import { Box } from '@mui/material'
|
9
|
+
import React from 'react'
|
10
|
+
import { DownloadableCartItemOptions } from '../components/DownloadableCartItemOptions/DownloadableCartItemOptions'
|
11
|
+
|
12
|
+
export const config: PluginConfig = {
|
13
|
+
type: 'component',
|
14
|
+
module: '@graphcommerce/magento-cart-items',
|
15
|
+
}
|
16
|
+
|
17
|
+
export function CartItemActionCard(props: PluginProps<CartItemActionCardProps>) {
|
18
|
+
const { Prev, ...rest } = props
|
19
|
+
|
20
|
+
return (
|
21
|
+
<Prev
|
22
|
+
{...rest}
|
23
|
+
details={
|
24
|
+
<>
|
25
|
+
{rest.details} <DownloadableCartItemOptions {...rest} />
|
26
|
+
</>
|
27
|
+
}
|
28
|
+
/>
|
29
|
+
)
|
30
|
+
}
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import { useWatch } from '@graphcommerce/ecommerce-ui'
|
2
|
+
import {
|
3
|
+
useFormAddProductsToCart,
|
4
|
+
type AddToCartItemSelector,
|
5
|
+
type ProductPagePriceProps,
|
6
|
+
} from '@graphcommerce/magento-product'
|
7
|
+
import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
|
8
|
+
import type { DownloadableProductOptionsFragment } from '../components/DownloadableProductOptions/DownloadableProductOptions.gql'
|
9
|
+
import type { ProductListItemDownloadableFragment } from '../components/ProductListItemDownloadable/ProductListItemDownloadable.gql'
|
10
|
+
|
11
|
+
export const config: PluginConfig = {
|
12
|
+
type: 'component',
|
13
|
+
module: '@graphcommerce/magento-product',
|
14
|
+
}
|
15
|
+
|
16
|
+
function isDownloadableProduct(
|
17
|
+
product:
|
18
|
+
| ProductPagePriceProps['product']
|
19
|
+
| (ProductPagePriceProps['product'] & DownloadableProductOptionsFragment),
|
20
|
+
): product is ProductPagePriceProps['product'] & DownloadableProductOptionsFragment {
|
21
|
+
return (
|
22
|
+
product.__typename === 'DownloadableProduct' &&
|
23
|
+
Array.isArray((product as DownloadableProductOptionsFragment).downloadable_product_links)
|
24
|
+
)
|
25
|
+
}
|
26
|
+
|
27
|
+
export function ProductPagePrice(
|
28
|
+
props: PluginProps<ProductPagePriceProps> & AddToCartItemSelector,
|
29
|
+
) {
|
30
|
+
const { Prev, product, index = 0, ...rest } = props
|
31
|
+
|
32
|
+
const form = useFormAddProductsToCart()
|
33
|
+
const selectedOptions = useWatch({
|
34
|
+
control: form.control,
|
35
|
+
name: `cartItems.${index}.selected_options`,
|
36
|
+
})
|
37
|
+
|
38
|
+
if (!isDownloadableProduct(product)) return <Prev product={product} index={index} {...rest} />
|
39
|
+
|
40
|
+
const selectedLinks = product.downloadable_product_links?.filter((link) =>
|
41
|
+
selectedOptions?.includes(link?.uid ?? ''),
|
42
|
+
)
|
43
|
+
|
44
|
+
const totalPrice = selectedLinks?.reduce((acc, link) => acc + (link?.price ?? 0), 0) ?? 0
|
45
|
+
|
46
|
+
return (
|
47
|
+
<Prev
|
48
|
+
product={{
|
49
|
+
...product,
|
50
|
+
price_range: {
|
51
|
+
...product.price_range,
|
52
|
+
minimum_price: {
|
53
|
+
...product.price_range.minimum_price,
|
54
|
+
regular_price: {
|
55
|
+
currency: product.price_range.minimum_price.regular_price.currency,
|
56
|
+
value: (product.price_range.minimum_price.regular_price.value ?? 0) + totalPrice,
|
57
|
+
},
|
58
|
+
final_price: {
|
59
|
+
currency: product.price_range.minimum_price.final_price.currency,
|
60
|
+
value: (product.price_range.minimum_price.final_price.value ?? 0) + totalPrice,
|
61
|
+
},
|
62
|
+
},
|
63
|
+
},
|
64
|
+
}}
|
65
|
+
index={index}
|
66
|
+
{...rest}
|
67
|
+
/>
|
68
|
+
)
|
69
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import type { CartItemInput } from '@graphcommerce/graphql-mesh'
|
2
|
+
import { type cartItemToCartItemInput as cartItemToCartItemInputType } from '@graphcommerce/magento-cart-items'
|
3
|
+
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
4
|
+
import { filterNonNullableKeys, isTypename } from '@graphcommerce/next-ui'
|
5
|
+
|
6
|
+
export const config: PluginConfig = {
|
7
|
+
type: 'function',
|
8
|
+
module: '@graphcommerce/magento-cart-items',
|
9
|
+
}
|
10
|
+
|
11
|
+
export const cartItemToCartItemInput: FunctionPlugin<typeof cartItemToCartItemInputType> = (
|
12
|
+
prev,
|
13
|
+
props,
|
14
|
+
) => {
|
15
|
+
const result = prev(props)
|
16
|
+
const { product, cartItem } = props
|
17
|
+
|
18
|
+
if (!result) return result
|
19
|
+
if (!isTypename(product, ['DownloadableProduct'])) return result
|
20
|
+
if (!isTypename(cartItem, ['DownloadableCartItem'])) return result
|
21
|
+
|
22
|
+
const links = filterNonNullableKeys(cartItem.links, ['title', 'price'])
|
23
|
+
const productLinks = filterNonNullableKeys(product.downloadable_product_links, ['title', 'price'])
|
24
|
+
|
25
|
+
const selected_options: NonNullable<CartItemInput['selected_options']> = []
|
26
|
+
|
27
|
+
productLinks.forEach((link) => {
|
28
|
+
const linkIndex = links.findIndex((l) => l.uid === link.uid)
|
29
|
+
if (linkIndex !== -1) selected_options[linkIndex] = link.uid
|
30
|
+
})
|
31
|
+
|
32
|
+
return {
|
33
|
+
...result,
|
34
|
+
selected_options,
|
35
|
+
} satisfies CartItemInput
|
36
|
+
}
|
@@ -1,22 +0,0 @@
|
|
1
|
-
fragment ProductPageDownloadableQueryFragment on Query {
|
2
|
-
typeProducts: products(filter: { url_key: { eq: $urlKey } }) {
|
3
|
-
items {
|
4
|
-
__typename
|
5
|
-
uid
|
6
|
-
... on DownloadableProduct {
|
7
|
-
downloadable_product_links {
|
8
|
-
price
|
9
|
-
sample_url
|
10
|
-
sort_order
|
11
|
-
title
|
12
|
-
uid
|
13
|
-
}
|
14
|
-
downloadable_product_samples {
|
15
|
-
title
|
16
|
-
sort_order
|
17
|
-
sample_url
|
18
|
-
}
|
19
|
-
}
|
20
|
-
}
|
21
|
-
}
|
22
|
-
}
|
File without changes
|
File without changes
|