@faststore/core 3.43.0 → 3.44.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.
Files changed (188) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +55 -55
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/server-production/0.pack +0 -0
  8. package/.next/cache/webpack/server-production/index.pack +0 -0
  9. package/.next/prerender-manifest.js +1 -1
  10. package/.next/prerender-manifest.json +1 -1
  11. package/.next/react-loadable-manifest.json +63 -63
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/1506.js +1 -1
  14. package/.next/server/chunks/2443.js +1 -0
  15. package/.next/server/chunks/2524.js +1 -0
  16. package/.next/server/chunks/4289.js +2 -362
  17. package/.next/server/chunks/4559.js +361 -0
  18. package/.next/server/chunks/4725.js +1 -0
  19. package/.next/server/chunks/4746.js +1 -1
  20. package/.next/server/chunks/5474.js +6 -0
  21. package/.next/server/chunks/5607.js +1 -0
  22. package/.next/server/chunks/563.js +1 -1
  23. package/.next/server/chunks/6594.js +1 -1
  24. package/.next/server/chunks/674.js +1 -1
  25. package/.next/server/chunks/6968.js +3 -2
  26. package/.next/server/chunks/7986.js +6 -1
  27. package/.next/server/chunks/804.js +1 -0
  28. package/.next/server/chunks/831.js +1 -1
  29. package/.next/server/chunks/8562.js +1 -1
  30. package/.next/server/chunks/8646.js +1 -1
  31. package/.next/server/chunks/ButtonSignIn.js +1 -1
  32. package/.next/server/chunks/UIBannerText.js +1 -1
  33. package/.next/server/chunks/UISKUMatrixSidebar.js +1 -1
  34. package/.next/server/middleware-build-manifest.js +1 -1
  35. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  36. package/.next/server/pages/404.js +1 -1
  37. package/.next/server/pages/404.js.nft.json +1 -1
  38. package/.next/server/pages/500.js +1 -1
  39. package/.next/server/pages/500.js.nft.json +1 -1
  40. package/.next/server/pages/[...slug].js +1 -1
  41. package/.next/server/pages/[...slug].js.nft.json +1 -1
  42. package/.next/server/pages/[slug]/p.js +1 -1
  43. package/.next/server/pages/[slug]/p.js.nft.json +1 -1
  44. package/.next/server/pages/_app.js +1 -1
  45. package/.next/server/pages/_app.js.nft.json +1 -1
  46. package/.next/server/pages/_document.js +1 -1
  47. package/.next/server/pages/_document.js.nft.json +1 -1
  48. package/.next/server/pages/_error.js +1 -1
  49. package/.next/server/pages/_error.js.nft.json +1 -1
  50. package/.next/server/pages/account/profile.js +1 -1
  51. package/.next/server/pages/account/profile.js.nft.json +1 -1
  52. package/.next/server/pages/account.js +1 -1
  53. package/.next/server/pages/account.js.nft.json +1 -1
  54. package/.next/server/pages/api/graphql.js +2 -1
  55. package/.next/server/pages/api/graphql.js.nft.json +1 -1
  56. package/.next/server/pages/api/health/live.js.nft.json +1 -1
  57. package/.next/server/pages/api/health/ready.js.nft.json +1 -1
  58. package/.next/server/pages/api/preview.js.nft.json +1 -1
  59. package/.next/server/pages/checkout.js +1 -1
  60. package/.next/server/pages/checkout.js.nft.json +1 -1
  61. package/.next/server/pages/en-US/404.html +2 -2
  62. package/.next/server/pages/en-US/500.html +2 -2
  63. package/.next/server/pages/en-US/checkout.html +2 -2
  64. package/.next/server/pages/en-US/login.html +2 -2
  65. package/.next/server/pages/en-US/s.html +2 -2
  66. package/.next/server/pages/en-US.html +2 -2
  67. package/.next/server/pages/index.js +1 -1
  68. package/.next/server/pages/index.js.nft.json +1 -1
  69. package/.next/server/pages/login.js +1 -1
  70. package/.next/server/pages/login.js.nft.json +1 -1
  71. package/.next/server/pages/s.js +1 -1
  72. package/.next/server/pages/s.js.nft.json +1 -1
  73. package/.next/static/U_9W0qMyv_4RQSLFdrtHC/_buildManifest.js +1 -0
  74. package/.next/static/chunks/{1153.ed17e19083f51bdb.js → 1153.8f0ac867c4ddce90.js} +1 -1
  75. package/.next/static/chunks/1173.ca14a5bc4e1264f4.js +1 -0
  76. package/.next/static/chunks/1418-e86227bcf9a5d486.js +28 -0
  77. package/.next/static/chunks/1552.8751332da83cadc5.js +1 -0
  78. package/.next/static/chunks/{6076.d80e37e35892bafc.js → 3684.3e6f435506c2c7c9.js} +1 -1
  79. package/.next/static/chunks/417.edbb75ab2f55482f.js +1 -0
  80. package/.next/static/chunks/{4349-6fc936580b772e67.js → 4349-87a956a73b97519c.js} +1 -1
  81. package/.next/static/chunks/{4625-9a273e78866c389b.js → 4625-553d20c86b0ca577.js} +1 -1
  82. package/.next/static/chunks/{4746.19eddeaeb33ea820.js → 4746.2e16ec0663a3873f.js} +1 -1
  83. package/.next/static/chunks/{4774.907a71a04126d57c.js → 4774.8e70c01dcd0ed4b3.js} +1 -1
  84. package/.next/static/chunks/{4865.3e2ae9feb511c870.js → 4865.4017be0f8e7c8cf0.js} +1 -1
  85. package/.next/static/chunks/5836.c5c244a8cbfa7f7d.js +1 -0
  86. package/.next/static/chunks/6335-6eb4ff84af184b88.js +1 -0
  87. package/.next/static/chunks/6536.d5260107222db13c.js +1 -0
  88. package/.next/static/chunks/{7195.e88924bd232c58b7.js → 7195.fd4cacc0bef1a233.js} +1 -1
  89. package/.next/static/chunks/7498-72289e704e7c7056.js +1 -0
  90. package/.next/static/chunks/7959.7c721505286572c0.js +1 -0
  91. package/.next/static/chunks/9.89f24163370f3a22.js +1 -0
  92. package/.next/static/chunks/BannerNewsletter.8982f42e60ed8828.js +1 -0
  93. package/.next/static/chunks/{ButtonSignIn.df0e3ce490436fe2.js → ButtonSignIn.bff8dc7b2133806c.js} +1 -1
  94. package/.next/static/chunks/CartItem.f2493be7c504f22f.js +1 -0
  95. package/.next/static/chunks/{CartSidebar.26d3e39bf93a368f.js → CartSidebar.aa9e43be37034654.js} +1 -1
  96. package/.next/static/chunks/{Gift.affb75f8f7bcb6db.js → Gift.375356d6a74e2693.js} +1 -1
  97. package/.next/static/chunks/Newsletter.9db4df2d4fb33227.js +1 -0
  98. package/.next/static/chunks/OrderSummary.e6e0a3af3c224b4e.js +1 -0
  99. package/.next/static/chunks/ProductShelf.9cea36116836c161.js +1 -0
  100. package/.next/static/chunks/ProductTiles.059a4a6360e1d4ea.js +1 -0
  101. package/.next/static/chunks/RegionModal.4594b707a1092551.js +1 -0
  102. package/.next/static/chunks/UIBannerText.45cf608e46cde628.js +1 -0
  103. package/.next/static/chunks/UIButton.3186f933c0ecfb31.js +1 -0
  104. package/.next/static/chunks/UISKUMatrixSidebar.9ac3980a60cfc358.js +1 -0
  105. package/.next/static/chunks/UIToast.221eb10681d2f642.js +1 -0
  106. package/.next/static/chunks/pages/{404-8e61ea158e7b314c.js → 404-6300c433469b7262.js} +1 -1
  107. package/.next/static/chunks/pages/{500-68f51177e64de6e7.js → 500-2d1dd344c8a8827f.js} +1 -1
  108. package/.next/static/chunks/pages/[...slug]-c1a62d58f5940747.js +1 -0
  109. package/.next/static/chunks/pages/[slug]/p-555dc09a5e78260e.js +1 -0
  110. package/.next/static/chunks/pages/_app-929466ed763a6d46.js +1 -0
  111. package/.next/static/chunks/pages/account/{profile-1fdb500de8f12b3a.js → profile-23b2e9fbbd95a30b.js} +1 -1
  112. package/.next/static/chunks/pages/{checkout-0dfe0dc9cee443ba.js → checkout-93b647fc45454d97.js} +1 -1
  113. package/.next/static/chunks/pages/{index-d36123e232b889d8.js → index-07bc320fc045538c.js} +1 -1
  114. package/.next/static/chunks/pages/{login-b59ea5561defc0d4.js → login-2454f1cd309c27d2.js} +1 -1
  115. package/.next/static/chunks/pages/{s-6120de2f57d674fc.js → s-9901e6857f517b8b.js} +1 -1
  116. package/.next/static/chunks/webpack-c2eed1e0b727b3ee.js +1 -0
  117. package/.next/static/css/b8945ac5f5327c34.css +1 -0
  118. package/.next/static/css/{f03f8973f7c72071.css → dbbb10bf2f162a58.css} +1 -1
  119. package/.next/trace +113 -111
  120. package/.turbo/turbo-build.log +25 -25
  121. package/.turbo/turbo-test.log +5 -5
  122. package/@generated/gql.ts +18 -2
  123. package/@generated/graphql.ts +58 -1
  124. package/@generated/persisted-documents.json +3 -1
  125. package/@generated/schema.graphql +14 -0
  126. package/CHANGELOG.md +6 -0
  127. package/cms/faststore/sections.json +110 -1
  128. package/discovery.config.default.js +6 -0
  129. package/package.json +6 -6
  130. package/src/Layout.tsx +2 -0
  131. package/src/components/cms/global/Components.ts +2 -0
  132. package/src/components/region/RegionBar/RegionBar.tsx +35 -2
  133. package/src/components/region/RegionButton/RegionButton.tsx +41 -6
  134. package/src/components/region/RegionModal/RegionModal.tsx +46 -34
  135. package/src/components/region/RegionModal/useRegion.ts +79 -0
  136. package/src/components/region/RegionModal/useRegionModal.ts +44 -0
  137. package/src/components/region/RegionPopover/RegionPopover.tsx +174 -0
  138. package/src/components/region/RegionPopover/index.ts +1 -0
  139. package/src/components/region/RegionPopover/section.module.scss +9 -0
  140. package/src/components/search/SearchInput/SearchInput.tsx +2 -2
  141. package/src/components/sections/Navbar/section.module.scss +3 -0
  142. package/src/components/sections/RegionBar/RegionBar.tsx +8 -3
  143. package/src/constants.ts +1 -0
  144. package/src/pages/[slug]/p.tsx +2 -1
  145. package/src/pages/_app.tsx +4 -2
  146. package/src/sdk/geolocation/useGeolocation.ts +42 -0
  147. package/src/sdk/product/index.ts +21 -0
  148. package/src/sdk/profile/index.ts +31 -0
  149. package/src/sdk/session/index.ts +37 -0
  150. package/src/sdk/ui/useOnClickOutside.ts +3 -38
  151. package/src/utils/utilities.ts +9 -1
  152. package/test/server/index.test.ts +1 -0
  153. package/.next/server/chunks/3105.js +0 -6
  154. package/.next/server/chunks/3779.js +0 -1
  155. package/.next/server/chunks/4444.js +0 -6
  156. package/.next/server/chunks/6030.js +0 -1
  157. package/.next/server/chunks/7337.js +0 -1
  158. package/.next/server/chunks/7675.js +0 -1
  159. package/.next/server/chunks/9594.js +0 -1
  160. package/.next/static/chunks/1173.5e86a9295010b785.js +0 -1
  161. package/.next/static/chunks/1552.b5a073e7ac834965.js +0 -1
  162. package/.next/static/chunks/3684.ba7a2a994988063b.js +0 -1
  163. package/.next/static/chunks/417.c39c1c5e5ef57b4a.js +0 -1
  164. package/.next/static/chunks/6167-ecb49640dcb9d566.js +0 -28
  165. package/.next/static/chunks/6335-5870fc075bf96b86.js +0 -1
  166. package/.next/static/chunks/6536.cd75ac69c241a35c.js +0 -1
  167. package/.next/static/chunks/7498-415859c993f5002b.js +0 -1
  168. package/.next/static/chunks/7959.9d31abae51a7e419.js +0 -1
  169. package/.next/static/chunks/8827.179b49f8ab3afe48.js +0 -1
  170. package/.next/static/chunks/9.e42e9b7463b94493.js +0 -1
  171. package/.next/static/chunks/BannerNewsletter.06b2b9724aa3c3ea.js +0 -1
  172. package/.next/static/chunks/CartItem.44cc90c698a69889.js +0 -1
  173. package/.next/static/chunks/Newsletter.d775e641d99322e6.js +0 -1
  174. package/.next/static/chunks/OrderSummary.8f34504b20564fec.js +0 -1
  175. package/.next/static/chunks/ProductShelf.0b5a71a93bca3148.js +0 -1
  176. package/.next/static/chunks/ProductTiles.a75612c0bcf990d6.js +0 -1
  177. package/.next/static/chunks/RegionModal.68b05bfd591d27fc.js +0 -1
  178. package/.next/static/chunks/UIBannerText.5ee380812ac3772c.js +0 -1
  179. package/.next/static/chunks/UIButton.c7a3273ac6ee5da5.js +0 -1
  180. package/.next/static/chunks/UISKUMatrixSidebar.c68540bda6d40e13.js +0 -1
  181. package/.next/static/chunks/UIToast.85ae30e3e8cc5ec7.js +0 -1
  182. package/.next/static/chunks/pages/[...slug]-2cfb2b1b8ee3b7a9.js +0 -1
  183. package/.next/static/chunks/pages/[slug]/p-3b513ae37c648620.js +0 -1
  184. package/.next/static/chunks/pages/_app-f02182ccd58f2781.js +0 -1
  185. package/.next/static/chunks/webpack-0dd5a14ceff64065.js +0 -1
  186. package/.next/static/css/31fb64e064998460.css +0 -1
  187. package/.next/static/f5jWOXDXh3GdBy9EK8IDc/_buildManifest.js +0 -1
  188. /package/.next/static/{f5jWOXDXh3GdBy9EK8IDc → U_9W0qMyv_4RQSLFdrtHC}/_ssgManifest.js +0 -0
@@ -1,9 +1,14 @@
1
1
  import type { RegionBarProps as UIRegionBarProps } from '@faststore/ui'
2
+ import { useEffect, useRef } from 'react'
2
3
 
3
4
  import { useUI } from '@faststore/ui'
4
5
  import { useSession } from 'src/sdk/session'
5
6
 
7
+ import { deliveryPromise, session as initialSession } from 'discovery.config'
6
8
  import { useOverrideComponents } from 'src/sdk/overrides/OverrideContext'
9
+ import { textToTitleCase } from 'src/utils/utilities'
10
+
11
+ import { useRegionModal } from '../RegionModal/useRegionModal'
7
12
 
8
13
  export interface RegionBarProps {
9
14
  /**
@@ -43,8 +48,34 @@ function RegionBar({
43
48
  ButtonIcon,
44
49
  } = useOverrideComponents<'RegionBar'>()
45
50
 
46
- const { openModal } = useUI()
47
- const { postalCode } = useSession()
51
+ const { openModal, openPopover } = useUI()
52
+ const { city, postalCode } = useSession()
53
+ const { isValidationComplete } = useRegionModal()
54
+ const regionBarRef = useRef<HTMLDivElement>(null)
55
+
56
+ const defaultPostalCode =
57
+ !!initialSession?.postalCode && postalCode === initialSession.postalCode
58
+
59
+ // If location is not mandatory, and default zipCode is provided or if the user has not set a zipCode, show the popover.
60
+ const displayRegionPopover =
61
+ defaultPostalCode || (!postalCode && !deliveryPromise.mandatory)
62
+
63
+ useEffect(() => {
64
+ if (!deliveryPromise.enabled) {
65
+ return
66
+ }
67
+
68
+ if (!isValidationComplete) {
69
+ return
70
+ }
71
+
72
+ if (isValidationComplete && displayRegionPopover && regionBarRef.current) {
73
+ openPopover({
74
+ isOpen: true,
75
+ triggerRef: regionBarRef,
76
+ })
77
+ }
78
+ }, [isValidationComplete])
48
79
 
49
80
  return (
50
81
  <RegionBarWrapper.Component
@@ -69,7 +100,9 @@ function RegionBar({
69
100
  // This decision can be reviewed later if needed
70
101
  onButtonClick={openModal}
71
102
  postalCode={postalCode}
103
+ city={textToTitleCase(city ?? '')}
72
104
  {...otherProps}
105
+ ref={regionBarRef}
73
106
  />
74
107
  )
75
108
  }
@@ -1,21 +1,56 @@
1
- import { Button as UIButton } from '@faststore/ui'
1
+ import { useEffect, useRef } from 'react'
2
2
 
3
- import { Icon, useUI } from '@faststore/ui'
3
+ import { Button as UIButton, Icon as UIIcon, useUI } from '@faststore/ui'
4
+ import { deliveryPromise, session as initialSession } from 'discovery.config'
4
5
  import { useSession } from 'src/sdk/session'
6
+ import { textToTitleCase } from 'src/utils/utilities'
7
+
8
+ import { useRegionModal } from '../RegionModal/useRegionModal'
5
9
 
6
10
  function RegionButton({ icon, label }: { icon: string; label: string }) {
7
- const { openModal } = useUI()
8
- const { postalCode } = useSession()
11
+ const { openModal, openPopover } = useUI()
12
+ const { city, postalCode } = useSession()
13
+ const { isValidationComplete } = useRegionModal()
14
+ const regionButtonRef = useRef<HTMLButtonElement>(null)
15
+
16
+ const defaultPostalCode =
17
+ !!initialSession?.postalCode && postalCode === initialSession.postalCode
18
+
19
+ // If location is not mandatory, and default zipCode is provided or if the user has not set a zipCode, show the popover.
20
+ const displayRegionPopover =
21
+ defaultPostalCode || (!postalCode && !deliveryPromise.mandatory)
22
+
23
+ useEffect(() => {
24
+ if (!deliveryPromise.enabled) {
25
+ return
26
+ }
27
+
28
+ if (!isValidationComplete) {
29
+ return
30
+ }
31
+
32
+ if (
33
+ isValidationComplete &&
34
+ displayRegionPopover &&
35
+ regionButtonRef.current
36
+ ) {
37
+ openPopover({
38
+ isOpen: true,
39
+ triggerRef: regionButtonRef,
40
+ })
41
+ }
42
+ }, [isValidationComplete])
9
43
 
10
44
  return (
11
45
  <UIButton
12
46
  variant="tertiary"
13
47
  size="small"
14
- icon={<Icon name={icon} width={18} height={18} weight="bold" />}
48
+ icon={<UIIcon name={icon} width={18} height={18} weight="bold" />}
15
49
  iconPosition="left"
16
50
  onClick={openModal}
51
+ ref={regionButtonRef}
17
52
  >
18
- {postalCode ?? label}
53
+ {city && postalCode ? `${textToTitleCase(city)}, ${postalCode}` : label}
19
54
  </UIButton>
20
55
  )
21
56
  }
@@ -1,13 +1,14 @@
1
- import {
2
- Icon,
3
- type RegionModalProps as UIRegionModalProps,
4
- useUI,
5
- } from '@faststore/ui'
1
+ import dynamic from 'next/dynamic'
6
2
  import { useRef, useState } from 'react'
7
3
 
8
- import { sessionStore, useSession, validateSession } from 'src/sdk/session'
4
+ import type { RegionModalProps as UIRegionModalProps } from '@faststore/ui'
5
+ import { Icon, useUI } from '@faststore/ui'
6
+
7
+ import { deliveryPromise } from 'discovery.config'
8
+ import { useSession } from 'src/sdk/session'
9
+
10
+ import useRegion from './useRegion'
9
11
 
10
- import dynamic from 'next/dynamic'
11
12
  import styles from './section.module.scss'
12
13
 
13
14
  const UIRegionModal = dynamic<UIRegionModalProps>(
@@ -17,7 +18,6 @@ const UIRegionModal = dynamic<UIRegionModalProps>(
17
18
  ),
18
19
  { ssr: false }
19
20
  )
20
-
21
21
  interface RegionModalProps {
22
22
  title?: UIRegionModalProps['title']
23
23
  description?: UIRegionModalProps['description']
@@ -25,6 +25,8 @@ interface RegionModalProps {
25
25
  inputField?: {
26
26
  label?: UIRegionModalProps['inputLabel']
27
27
  errorMessage?: UIRegionModalProps['errorMessage']
28
+ noProductsAvailableErrorMessage?: UIRegionModalProps['errorMessage']
29
+ buttonActionText?: UIRegionModalProps['inputButtonActionText']
28
30
  }
29
31
  idkPostalCodeLink?: {
30
32
  text?: string
@@ -40,7 +42,12 @@ function RegionModal({
40
42
  title,
41
43
  description,
42
44
  closeButtonAriaLabel,
43
- inputField: { label: inputFieldLabel, errorMessage: inputFieldErrorMessage },
45
+ inputField: {
46
+ label: inputFieldLabel,
47
+ errorMessage: inputFieldErrorMessage,
48
+ noProductsAvailableErrorMessage: inputFieldNoProductsAvailableErrorMessage,
49
+ buttonActionText: inputButtonActionText,
50
+ },
44
51
  idkPostalCodeLink: {
45
52
  text: idkPostalCodeLinkText,
46
53
  to: idkPostalCodeLinkTo,
@@ -49,35 +56,33 @@ function RegionModal({
49
56
  }: RegionModalProps) {
50
57
  const inputRef = useRef<HTMLInputElement>(null)
51
58
  const { isValidating, ...session } = useSession()
52
- const [errorMessage, setErrorMessage] = useState<string>('')
59
+ const { modal: displayModal, closeModal } = useUI()
60
+
53
61
  const [input, setInput] = useState<string>('')
54
- const { modal: displayModal } = useUI()
55
62
 
56
- const handleSubmit = async () => {
57
- const postalCode = inputRef.current?.value
63
+ const { loading, setRegion, regionError, setRegionError } = useRegion()
58
64
 
59
- if (typeof postalCode !== 'string') {
65
+ const handleSubmit = async () => {
66
+ if (isValidating) {
60
67
  return
61
68
  }
62
69
 
63
- setErrorMessage('')
64
-
65
- try {
66
- const newSession = {
67
- ...session,
68
- postalCode,
69
- }
70
-
71
- const validatedSession = await validateSession(newSession)
72
-
73
- sessionStore.set(validatedSession ?? newSession)
74
- } catch (error) {
75
- setErrorMessage(inputFieldErrorMessage)
76
- }
70
+ await setRegion({
71
+ session,
72
+ onSuccess: () => {
73
+ setInput('')
74
+ closeModal()
75
+ },
76
+ postalCode: input,
77
+ errorMessage: inputFieldErrorMessage,
78
+ noProductsAvailableErrorMessage:
79
+ inputFieldNoProductsAvailableErrorMessage,
80
+ })
77
81
  }
78
82
 
83
+ const isDismissible = !!(!deliveryPromise?.mandatory || session.postalCode)
79
84
  const idkPostalCodeLinkProps: UIRegionModalProps['idkPostalCodeLinkProps'] = {
80
- href: idkPostalCodeLinkTo ?? '#',
85
+ href: idkPostalCodeLinkTo,
81
86
  children: (
82
87
  <>
83
88
  {idkPostalCodeLinkText}
@@ -106,15 +111,22 @@ function RegionModal({
106
111
  inputRef={inputRef}
107
112
  inputValue={input}
108
113
  inputLabel={inputFieldLabel}
109
- errorMessage={errorMessage}
110
- idkPostalCodeLinkProps={idkPostalCodeLinkProps}
114
+ errorMessage={regionError}
115
+ idkPostalCodeLinkProps={
116
+ idkPostalCodeLinkTo ? idkPostalCodeLinkProps : null
117
+ }
111
118
  onInput={(e) => {
112
- errorMessage !== '' && setErrorMessage('')
119
+ regionError !== '' && setRegionError('')
113
120
  setInput(e.currentTarget.value)
114
121
  }}
115
122
  onSubmit={handleSubmit}
116
- fadeOutOnSubmit={true}
117
- onClear={() => setInput('')}
123
+ fadeOutOnSubmit={false}
124
+ onClear={() => {
125
+ setInput('')
126
+ setRegionError('')
127
+ }}
128
+ inputButtonActionText={loading ? '...' : inputButtonActionText}
129
+ dismissible={isDismissible}
118
130
  />
119
131
  )}
120
132
  </>
@@ -0,0 +1,79 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ import type { Session } from '@faststore/sdk'
4
+ import { sessionStore, validateSession } from 'src/sdk/session'
5
+ import { getProductCount } from 'src/sdk/product'
6
+ import { deliveryPromise } from 'discovery.config'
7
+
8
+ type SetRegionProps = {
9
+ session: Session
10
+ postalCode: string | undefined
11
+ onSuccess?: () => void
12
+ errorMessage: string
13
+ noProductsAvailableErrorMessage?: string
14
+ }
15
+
16
+ type UseRegionValues = {
17
+ loading: boolean
18
+ regionError: string
19
+ setRegion: (props: SetRegionProps) => Promise<void>
20
+ setRegionError: (value: string) => void
21
+ }
22
+
23
+ export default function useRegion(): UseRegionValues {
24
+ const [loading, setLoading] = useState<boolean>(false)
25
+ const [regionError, setRegionError] = useState<string>('')
26
+
27
+ const setRegion = async ({
28
+ postalCode,
29
+ errorMessage,
30
+ session,
31
+ onSuccess,
32
+ noProductsAvailableErrorMessage,
33
+ }: SetRegionProps) => {
34
+ if (typeof postalCode !== 'string') {
35
+ return
36
+ }
37
+
38
+ setLoading(true)
39
+
40
+ try {
41
+ const newSession = {
42
+ ...session,
43
+ postalCode,
44
+ geoCoordinates: null, // Revalidate geo coordinates in API when users set a new postal code
45
+ } as Session
46
+
47
+ const validatedSession = await validateSession(newSession)
48
+
49
+ if (deliveryPromise.enabled) {
50
+ // Check product availability for specific postal code
51
+ const productCount = await getProductCount()
52
+ if (productCount === 0) {
53
+ const errorFallback = `There are no products available for ${postalCode}.`
54
+ const noProductsAvailableError =
55
+ noProductsAvailableErrorMessage?.replace(/%s/g, () => postalCode)
56
+
57
+ setRegionError(noProductsAvailableError ?? errorFallback)
58
+ setLoading(false)
59
+ return
60
+ }
61
+ }
62
+
63
+ sessionStore.set(validatedSession ?? newSession)
64
+ setRegionError('')
65
+ onSuccess?.() // Execute the post-validation action (close modal, etc.)
66
+ } catch (error) {
67
+ setRegionError(errorMessage)
68
+ } finally {
69
+ setLoading(false) // Reset loading to false when validation is complete
70
+ }
71
+ }
72
+
73
+ return {
74
+ loading,
75
+ setRegion,
76
+ regionError,
77
+ setRegionError,
78
+ }
79
+ }
@@ -0,0 +1,44 @@
1
+ import { useUI } from '@faststore/ui'
2
+ import { deliveryPromise } from 'discovery.config'
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { sessionStore, useSession } from 'src/sdk/session'
5
+
6
+ export function useRegionModal() {
7
+ const { openModal: displayRegionModal } = useUI()
8
+ const { isValidating } = useSession()
9
+
10
+ // Ref to track the previous value of isValidating
11
+ const prevIsValidating = useRef(isValidating)
12
+
13
+ // State to track if validation is complete
14
+ const [isValidationComplete, setValidationComplete] = useState(false)
15
+
16
+ const openRegionModal = () => {
17
+ const { postalCode } = sessionStore.read()
18
+ if (!postalCode) {
19
+ displayRegionModal()
20
+ }
21
+ }
22
+
23
+ // Effect to handle when isValidating changes from true to false
24
+ useEffect(() => {
25
+ if (!deliveryPromise.enabled) {
26
+ return
27
+ }
28
+
29
+ // Check if validation has completed (isValidating changed from true to false)
30
+ if (prevIsValidating.current && !isValidating) {
31
+ setValidationComplete(true)
32
+
33
+ // If the postal code is not set and is mandatory, open the region modal
34
+ if (deliveryPromise.mandatory) {
35
+ openRegionModal()
36
+ }
37
+ }
38
+
39
+ // Update the previous value of isValidating
40
+ prevIsValidating.current = isValidating
41
+ }, [openRegionModal])
42
+
43
+ return { openRegionModal, isValidationComplete }
44
+ }
@@ -0,0 +1,174 @@
1
+ import type { PopoverProps as UIPopoverProps } from '@faststore/ui'
2
+ import {
3
+ Icon as UIIcon,
4
+ InputField as UIInputField,
5
+ Link as UILink,
6
+ Popover as UIPopover,
7
+ useUI,
8
+ } from '@faststore/ui'
9
+ import { useRef, useState } from 'react'
10
+
11
+ import useRegion from '../RegionModal/useRegion'
12
+
13
+ import { sessionStore, useSession } from 'src/sdk/session'
14
+ import { textToTitleCase } from 'src/utils/utilities'
15
+ import styles from './section.module.scss'
16
+
17
+ interface RegionPopoverProps {
18
+ title?: UIPopoverProps['title']
19
+ closeButtonAriaLabel?: UIPopoverProps['closeButtonAriaLabel']
20
+ inputField?: {
21
+ label?: string
22
+ errorMessage?: string
23
+ noProductsAvailableErrorMessage?: string
24
+ buttonActionText?: string
25
+ }
26
+ idkPostalCodeLink?: {
27
+ text?: string
28
+ to?: string
29
+ icon?: {
30
+ icon?: string
31
+ alt?: string
32
+ }
33
+ }
34
+ textBeforeLocation?: string
35
+ textAfterLocation?: string
36
+ description?: string
37
+ triggerRef?: UIPopoverProps['triggerRef']
38
+ onDismiss: UIPopoverProps['onDismiss']
39
+ offsetTop?: UIPopoverProps['offsetTop']
40
+ offsetLeft?: UIPopoverProps['offsetLeft']
41
+ placement?: UIPopoverProps['placement']
42
+ }
43
+
44
+ function RegionPopover({
45
+ title = 'Set your location',
46
+ closeButtonAriaLabel,
47
+ inputField: {
48
+ label: inputFieldLabel,
49
+ errorMessage: inputFieldErrorMessage,
50
+ noProductsAvailableErrorMessage: inputFieldNoProductsAvailableErrorMessage,
51
+ buttonActionText: inputButtonActionText,
52
+ },
53
+ idkPostalCodeLink: {
54
+ text: idkPostalCodeLinkText,
55
+ to: idkPostalCodeLinkTo,
56
+ icon: { icon: idkPostalCodeLinkIcon, alt: idkPostalCodeLinkIconAlt },
57
+ },
58
+ textBeforeLocation = 'Your current location is:',
59
+ textAfterLocation = 'Use the field below to change it.',
60
+ description = 'Offers and availability vary by location.',
61
+ triggerRef,
62
+ offsetTop = 6,
63
+ offsetLeft,
64
+ placement = 'bottom-start',
65
+ }: RegionPopoverProps) {
66
+ const inputRef = useRef<HTMLInputElement>(null)
67
+ const { isValidating, ...session } = useSession()
68
+ const { popover: displayPopover, closePopover } = useUI()
69
+ const { city, postalCode } = sessionStore.read()
70
+ const location = city ? `${textToTitleCase(city)}, ${postalCode}` : postalCode
71
+
72
+ const [input, setInput] = useState<string>('')
73
+
74
+ const { loading, setRegion, regionError, setRegionError } = useRegion()
75
+
76
+ const handleSubmit = async () => {
77
+ if (isValidating) {
78
+ return
79
+ }
80
+
81
+ await setRegion({
82
+ session,
83
+ onSuccess: () => {
84
+ setInput('')
85
+ closePopover()
86
+ },
87
+ postalCode: input,
88
+ errorMessage: inputFieldErrorMessage,
89
+ noProductsAvailableErrorMessage:
90
+ inputFieldNoProductsAvailableErrorMessage,
91
+ })
92
+ }
93
+
94
+ const idkPostalCodeLinkProps = {
95
+ href: idkPostalCodeLinkTo,
96
+ children: (
97
+ <>
98
+ {idkPostalCodeLinkText}
99
+ {!!idkPostalCodeLinkIcon && (
100
+ <UIIcon
101
+ name={idkPostalCodeLinkIcon}
102
+ aria-label={idkPostalCodeLinkIconAlt}
103
+ width={20}
104
+ height={20}
105
+ />
106
+ )}
107
+ </>
108
+ ),
109
+ }
110
+
111
+ const RegionPopoverContent = (
112
+ <>
113
+ <span data-fs-region-popover-description>
114
+ {postalCode ? (
115
+ <>
116
+ {textBeforeLocation} <span>{location}</span>. {textAfterLocation}
117
+ </>
118
+ ) : (
119
+ <>{description}</>
120
+ )}
121
+ </span>
122
+ <UIInputField
123
+ data-fs-region-popover-input
124
+ id="region-popover-input-postal-code"
125
+ inputRef={inputRef}
126
+ label={inputFieldLabel}
127
+ actionable
128
+ value={input}
129
+ onInput={(e) => {
130
+ regionError !== '' && setRegionError('')
131
+ setInput(e.currentTarget.value)
132
+ }}
133
+ onSubmit={handleSubmit}
134
+ onClear={() => {
135
+ setInput('')
136
+ setRegionError('')
137
+ }}
138
+ buttonActionText={loading ? '...' : inputButtonActionText}
139
+ error={regionError}
140
+ />
141
+ {idkPostalCodeLinkTo && (
142
+ <UILink data-fs-region-popover-link {...idkPostalCodeLinkProps} />
143
+ )}
144
+ </>
145
+ )
146
+
147
+ return (
148
+ <>
149
+ {displayPopover.isOpen && (
150
+ <div className={`${styles.section} section-region-popover`}>
151
+ <UIPopover
152
+ data-fs-region-popover
153
+ title={title}
154
+ isOpen={displayPopover.isOpen}
155
+ content={RegionPopoverContent}
156
+ placement={placement}
157
+ dismissible
158
+ triggerRef={triggerRef}
159
+ offsetTop={offsetTop}
160
+ offsetLeft={offsetLeft}
161
+ closeButtonAriaLabel={closeButtonAriaLabel}
162
+ onEntered={() => {
163
+ if (!postalCode && inputRef.current) {
164
+ inputRef.current.focus()
165
+ }
166
+ }}
167
+ />
168
+ </div>
169
+ )}
170
+ </>
171
+ )
172
+ }
173
+
174
+ export default RegionPopover
@@ -0,0 +1 @@
1
+ export { default } from './RegionPopover'
@@ -0,0 +1,9 @@
1
+ .section {
2
+ @import "@faststore/ui/src/components/atoms/Icon/styles.scss";
3
+ @import "@faststore/ui/src/components/atoms/Input/styles.scss";
4
+ @import "@faststore/ui/src/components/atoms/Button/styles.scss";
5
+ @import "@faststore/ui/src/components/atoms/Link/styles.scss";
6
+ @import "@faststore/ui/src/components/molecules/InputField/styles.scss";
7
+ @import "@faststore/ui/src/components/molecules/Popover/styles.scss";
8
+ @import "@faststore/ui/src/components/organisms/RegionPopover/styles.scss";
9
+ }
@@ -18,6 +18,7 @@ import {
18
18
  Icon as UIIcon,
19
19
  IconButton as UIIconButton,
20
20
  SearchInput as UISearchInput,
21
+ useOnClickOutside,
21
22
  } from '@faststore/ui'
22
23
 
23
24
  import type {
@@ -27,10 +28,9 @@ import type {
27
28
 
28
29
  import type { SearchProviderContextValue } from '@faststore/ui'
29
30
 
31
+ import type { NavbarProps } from 'src/components/sections/Navbar'
30
32
  import useSearchHistory from 'src/sdk/search/useSearchHistory'
31
33
  import useSuggestions from 'src/sdk/search/useSuggestions'
32
- import useOnClickOutside from 'src/sdk/ui/useOnClickOutside'
33
- import type { NavbarProps } from 'src/components/sections/Navbar'
34
34
 
35
35
  import { formatSearchPath } from 'src/sdk/search/formatSearchPath'
36
36
 
@@ -14,10 +14,13 @@
14
14
  @import "@faststore/ui/src/components/atoms/Logo/styles.scss";
15
15
  @import "@faststore/ui/src/components/atoms/Overlay/styles.scss";
16
16
  @import "@faststore/ui/src/components/atoms/Price/styles.scss";
17
+ @import "@faststore/ui/src/components/molecules/InputField/styles.scss";
17
18
  @import "@faststore/ui/src/components/molecules/LinkButton/styles.scss";
18
19
  @import "@faststore/ui/src/components/molecules/QuantitySelector/styles.scss";
19
20
  @import "@faststore/ui/src/components/molecules/NavbarLinks/styles.scss";
21
+ @import "@faststore/ui/src/components/molecules/Popover/styles.scss";
20
22
  @import "@faststore/ui/src/components/molecules/ProductPrice/styles.scss";
23
+ @import "@faststore/ui/src/components/organisms/RegionPopover/styles.scss";
21
24
  @import "@faststore/ui/src/components/molecules/SearchAutoComplete/styles.scss";
22
25
  @import "@faststore/ui/src/components/molecules/SearchDropdown/styles.scss";
23
26
  @import "@faststore/ui/src/components/molecules/SearchHistory/styles.scss";
@@ -1,3 +1,4 @@
1
+ import useScreenResize from 'src/sdk/ui/useScreenResize'
1
2
  import { getOverridableSection } from '../../..//sdk/overrides/getOverriddenSection'
2
3
  import RegionBar, {
3
4
  type RegionBarProps,
@@ -30,10 +31,14 @@ type RegionBarSectionProps = {
30
31
  }
31
32
 
32
33
  function RegionBarSection({ ...otherProps }: RegionBarSectionProps) {
34
+ const { isDesktop } = useScreenResize()
35
+
33
36
  return (
34
- <Section className={`${styles.section} section-region-bar display-mobile`}>
35
- <RegionBar {...otherProps} />
36
- </Section>
37
+ !isDesktop && (
38
+ <Section className={`${styles.section} section-region-bar`}>
39
+ <RegionBar {...otherProps} />
40
+ </Section>
41
+ )
37
42
  )
38
43
  }
39
44
 
package/src/constants.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export const ITEMS_PER_PAGE = 12
2
2
  export const ITEMS_PER_SECTION = 5
3
+ export const TIME_TO_VALIDATE_SESSION = 3000
@@ -91,8 +91,9 @@ function Page({
91
91
  offers,
92
92
  meta,
93
93
  }: Props) {
94
- const { product } = server
95
94
  const { currency } = useSession()
95
+
96
+ const { product } = server
96
97
  const {
97
98
  seo: { pdp: pdpSeo, ...storeSeo },
98
99
  } = storeConfig
@@ -3,17 +3,19 @@ import type { AppProps } from 'next/app'
3
3
  import Layout from 'src/Layout'
4
4
  import AnalyticsHandler from 'src/sdk/analytics'
5
5
  import ErrorBoundary from 'src/sdk/error/ErrorBoundary'
6
+ import useGeolocation from 'src/sdk/geolocation/useGeolocation'
6
7
  import SEO from '../../next-seo.config'
7
8
 
8
9
  // FastStore UI's base styles
9
- import '../styles/global/index.scss'
10
- import '../plugins/index.scss'
11
10
  import '../customizations/src/themes/index.scss'
11
+ import '../plugins/index.scss'
12
+ import '../styles/global/index.scss'
12
13
 
13
14
  import { DefaultSeo } from 'next-seo'
14
15
 
15
16
  function App({ Component, pageProps }: AppProps) {
16
17
  const { key } = pageProps
18
+ useGeolocation()
17
19
 
18
20
  return (
19
21
  <ErrorBoundary>