Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new cart hook #147

Merged
merged 16 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-parents-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@elasticpath/react-shopper-hooks": minor
---

New interface for useCart
5 changes: 5 additions & 0 deletions .changeset/metal-queens-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@elasticpath/d2c-schematics": minor
---

fix shopper hooks version
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,59 @@ This endpoint doesn't require any parameters, although it support additional opt

### Filtering Products

You can filter products by using the `filter` query parameter. For example, to filter products by category, you can use the `category` filter:
You can filter products by using the `filter` query parameter. For example, to filter products by category, you can use the `category` filter:

## Get Product by ID

You can get a single product in your storefront using the [PXM catalog view (shopper) endpoints, get a product](https://elasticpath.dev/docs/pxm/catalogs/shopper-catalog/get-a-product).


<Tabs>
<TabItem value="js-sdk" label="JS SDK">

```typescript
client.ShopperCatalog.Products.Get({productId: productId}).then((product) => {
console.log(product.id)
})
```

</TabItem>

<TabItem value="elastic-path-react" label="Elastic Path React">

```tsx
import { useProduct } from "@elasticpath/react-shopper-hooks"

export default function Products() {
const { data: product, isLoading } = useProduct()

return (
<div>
{isLoading && <span>Loading...</span>}
{product && <span>{product.attributes.name}</span>}
</div>
)
}
```

</TabItem>
<TabItem value="fetch" label="Fetch API">

```js
fetch("https://useast.api.elasticpath.com/catalog/products/${productId}", {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer XXXX"
}
}).then(response => response.json())
.then(data => console.log(data));
```

</TabItem>
</Tabs>

Returns the specified product from the catalog. The product must be in the live status.

If you have multiple catalog rules defined, the rule that best matches the shopperʼs context is used to determine which catalog is retrieved. For information about how rules are matched, see [Resolving Catalog Rules](https://elasticpath.dev/docs/pxm/catalogs/shopper-catalog/catalog-shopper-overview#resolving-catalog-rules).

You can see the parent nodes a product is associated with in the bread_crumbs and bread_crumb_nodes metadata for each product. This is useful if you want to improve how your shoppers search your store, for example. For more information, see [Catalog Releases Overview](https://elasticpath.dev/docs/pxm/catalogs/catalog-latest-release/overview).
2 changes: 1 addition & 1 deletion apps/composable-cli-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"start": "docusaurus start --port 3005",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
Expand Down
4 changes: 2 additions & 2 deletions packages/d2c-schematics/utility/latest-versions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"private": true,
"dependencies": {
"@moltin/sdk": "^27.0.0",
"@elasticpath/react-shopper-hooks": "^0.6.2",
"@elasticpath/shopper-common": "^0.2.1",
"@elasticpath/react-shopper-hooks": "0.6.2",
"@elasticpath/shopper-common": "0.2.1",
"clsx": "^1.2.1",
"cookies-next": "^4.0.0",
"focus-visible": "^5.2.0",
Expand Down
44 changes: 41 additions & 3 deletions packages/react-shopper-hooks/src/account/account-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React, { createContext, ReactNode } from "react"
import { AccountMember } from "@moltin/sdk"
import Cookies from "js-cookie"
import { AccountCredentials } from "./types"
import { AccountTokenBase, AccountMember } from "@moltin/sdk"

interface AccountState {
export interface AccountState {
accountCookieName: string
profile: AccountMember | null
getSelectedAccountToken: () => AccountTokenBase | undefined
getAccountMemberTokens: () => Record<string, AccountTokenBase> | undefined
}

export const AccountProviderContext = createContext<AccountState | null>(null)
Expand All @@ -19,9 +23,43 @@ export const AccountProvider = ({
children,
accountCookieName = ACCOUNT_MEMBER_TOKEN_STR,
}: AccountProviderProps) => {
function getParsedCookie(): AccountCredentials | undefined {
const cookie = Cookies.get(accountCookieName)
return cookie && JSON.parse(cookie)
}

function getAccountMemberTokens():
| Record<string, AccountTokenBase>
| undefined {
const parsedCookie = getParsedCookie()

if (!parsedCookie) {
return undefined
}

return parsedCookie.accounts
}

function getSelectedAccountToken(): AccountTokenBase | undefined {
const parsedCookie = getParsedCookie()

if (!parsedCookie) {
return undefined
}

const token = parsedCookie.accounts[parsedCookie.selected]

return token
}

return (
<AccountProviderContext.Provider
value={{ accountCookieName, profile: null }}
value={{
accountCookieName,
getAccountMemberTokens,
getSelectedAccountToken,
profile: null
}}
>
{children}
</AccountProviderContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useElasticPath } from "../../elasticpath"
import { UseQueryOptionsWrapper } from "../../types"
import { AccountAddress, ResourcePage } from "@moltin/sdk"
import { useQuery, UseQueryResult } from "@tanstack/react-query"
import { queryKeysFactory } from "../../shared/util/query-keys-factory"

const ACCOUNT_ADDRESSES_QUERY_KEY = "account-addresses" as const

export const accountAddressesQueryKeys = queryKeysFactory(
ACCOUNT_ADDRESSES_QUERY_KEY,
)
type AccountAddressesQueryKey = typeof accountAddressesQueryKeys

export function useAccountAddresses(
accountId: string,
options?: UseQueryOptionsWrapper<
ResourcePage<AccountAddress>,
Error,
ReturnType<AccountAddressesQueryKey["list"] & string>
> & { ep?: { accountMemberToken?: string } },
): Partial<ResourcePage<AccountAddress>> &
Omit<UseQueryResult<ResourcePage<AccountAddress>, Error>, "data"> {
const { client } = useElasticPath()
const { data, ...rest } = useQuery({
queryKey: [...accountAddressesQueryKeys.list({ accountId })],
queryFn: () =>
client.AccountAddresses.All({
account: accountId,
...(options?.ep?.accountMemberToken && {
token: options.ep.accountMemberToken,
}),
}),
...options,
})

return { ...data, ...rest } as const
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { useElasticPath } from "../../elasticpath/elasticpath"
import { UseQueryOptionsWrapper } from "../../types"
import { AccountMember, Resource } from "@moltin/sdk"
import { useQuery } from "@tanstack/react-query"
import { useQuery, UseQueryResult } from "@tanstack/react-query"
import { queryKeysFactory } from "../../shared/util/query-keys-factory"
import { UseQueryResult } from "@tanstack/react-query/src/types"

const ACCOUNT_MEMBER_QUERY_KEY = "account-member" as const

export const accountMemberQueryKeys = queryKeysFactory(ACCOUNT_MEMBER_QUERY_KEY)
type AccountMemberQueryKey = typeof accountMemberQueryKeys
type Temp = UseQueryResult<Resource<AccountMember> | undefined, Error>

export function useAccountMember(
id: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
import { useAccountMember } from "./use-account-member"
import { useContext } from "react"
import { useContext, useEffect, useState } from "react"
import { AccountProviderContext } from "../account-provider"
import Cookies from "js-cookie"
import {
createCookieTokenStore,
resolveAccountMemberIdFromToken,
} from "../login-account"
import { useElasticPath } from "../../elasticpath/elasticpath"
import { AccountMember, Resource } from "@moltin/sdk"
import { AccountMember, AccountTokenBase, Resource } from "@moltin/sdk"
import { UseQueryResult } from "@tanstack/react-query/src/types"
import { AccountCredentials } from "../types"

export function useAuthedAccountMember(): Partial<Resource<AccountMember>> &
Omit<UseQueryResult<Resource<AccountMember>, Error>, "data"> {
Omit<UseQueryResult<Resource<AccountMember>, Error>, "data"> & {
accountMemberTokens?: Record<string, AccountTokenBase>
selectedAccountToken?: AccountTokenBase
} {
const ctx = useContext(AccountProviderContext)
const [accountMemberTokens, setAccountMemberTokens] = useState<
Record<string, AccountTokenBase> | undefined
>()
const [selectedAccountToken, setSelectedAccountToken] = useState<
AccountTokenBase | undefined
>()

if (!ctx) {
throw new Error(
"useAuthedAccountMember must be used within an AccountProvider",
)
}

const { client } = useElasticPath()

const tokenStore = createCookieTokenStore(ctx.accountCookieName)
const authedAccountMemberId = resolveAccountMemberIdFromToken(
client,
tokenStore,
)
const accountCookie = Cookies.get(ctx.accountCookieName)
const parsedAccountCookie: AccountCredentials | undefined =
accountCookie && JSON.parse(accountCookie)

const selectedAccount =
parsedAccountCookie?.accounts[parsedAccountCookie?.selected]

const result = useAccountMember(authedAccountMemberId ?? "", {
enabled: !!accountCookie && !!authedAccountMemberId,
ep: { accountMemberToken: accountCookie },
const result = useAccountMember(parsedAccountCookie?.accountMemberId ?? "", {
enabled: !!accountCookie && !!parsedAccountCookie?.accountMemberId,
ep: { accountMemberToken: selectedAccount?.token },
})

return { ...result } as const
useEffect(() => {
setAccountMemberTokens(ctx.getAccountMemberTokens())
setSelectedAccountToken(ctx.getSelectedAccountToken())
}, [result.data])

return {
...result,
accountMemberTokens,
selectedAccountToken,
} as const
}
1 change: 1 addition & 0 deletions packages/react-shopper-hooks/src/account/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./hooks/use-account-member"
export * from "./hooks/use-authed-account-member"
export * from "./account-provider"
export * from "./hooks/use-account-addresses"
30 changes: 7 additions & 23 deletions packages/react-shopper-hooks/src/account/login-account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AccountMember, Moltin, Resource } from "@moltin/sdk"
import { AccountMember, AccountTokenBase, Moltin, Resource } from "@moltin/sdk"
import Cookies from "js-cookie"
import jwtDecode from "jwt-decode"

Expand Down Expand Up @@ -57,7 +57,12 @@ export async function resolveAccountMember(
return undefined
}

const decodedToken = token ? jwtDecode<{ sub?: string }>(token) : undefined
const parsedToken: AccountTokenBase & { account_member_id: string } =
JSON.parse(token)

const decodedToken = parsedToken?.token
? jwtDecode<{ sub?: string }>(parsedToken.token)
: undefined

if (!decodedToken) {
return undefined
Expand Down Expand Up @@ -89,24 +94,3 @@ export async function resolveAccountMember(
return undefined
}
}

export function resolveAccountMemberIdFromToken(
client: Moltin,
tokenStore: TokenStore,
) {
const token = tokenStore.getToken()

if (!token) {
return undefined
}

const decodedToken = token ? jwtDecode<{ sub?: string }>(token) : undefined

if (!decodedToken) {
return undefined
}

const { sub: accountMemberId } = decodedToken

return accountMemberId
}
7 changes: 7 additions & 0 deletions packages/react-shopper-hooks/src/account/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AccountTokenBase } from "@moltin/sdk"

export type AccountCredentials = {
accountMemberId: string
accounts: Record<string, AccountTokenBase>
selected: string
}
Loading