@salesforce/commerce-sdk-react 1.3.0 → 1.4.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/README.md +62 -1
  3. package/auth/index.d.ts +21 -16
  4. package/auth/index.js +127 -57
  5. package/auth/storage/cookie.js +25 -1
  6. package/components/StorefrontPreview/storefront-preview.js +20 -0
  7. package/components/StorefrontPreview/utils.d.ts +6 -0
  8. package/components/StorefrontPreview/utils.js +20 -6
  9. package/constant.d.ts +3 -0
  10. package/constant.js +5 -2
  11. package/hooks/ShopperBaskets/paramKeys.d.ts +9 -0
  12. package/hooks/ShopperBaskets/paramKeys.js +25 -0
  13. package/hooks/ShopperBaskets/query.js +36 -5
  14. package/hooks/ShopperBaskets/queryKeyHelpers.d.ts +0 -5
  15. package/hooks/ShopperBaskets/queryKeyHelpers.js +22 -10
  16. package/hooks/ShopperContexts/paramKeys.d.ts +5 -0
  17. package/hooks/ShopperContexts/paramKeys.js +17 -0
  18. package/hooks/ShopperContexts/query.js +13 -1
  19. package/hooks/ShopperContexts/queryKeyHelpers.d.ts +0 -5
  20. package/hooks/ShopperContexts/queryKeyHelpers.js +6 -2
  21. package/hooks/ShopperCustomers/cache.js +7 -2
  22. package/hooks/ShopperCustomers/paramKeys.d.ts +26 -0
  23. package/hooks/ShopperCustomers/paramKeys.js +44 -0
  24. package/hooks/ShopperCustomers/query.js +73 -16
  25. package/hooks/ShopperCustomers/queryKeyHelpers.d.ts +0 -6
  26. package/hooks/ShopperCustomers/queryKeyHelpers.js +58 -28
  27. package/hooks/ShopperExperience/paramKeys.d.ts +6 -0
  28. package/hooks/ShopperExperience/paramKeys.js +19 -0
  29. package/hooks/ShopperExperience/query.js +18 -2
  30. package/hooks/ShopperExperience/queryKeyHelpers.d.ts +0 -5
  31. package/hooks/ShopperExperience/queryKeyHelpers.js +10 -4
  32. package/hooks/ShopperGiftCertificates/paramKeys.d.ts +5 -0
  33. package/hooks/ShopperGiftCertificates/paramKeys.js +17 -0
  34. package/hooks/ShopperGiftCertificates/query.js +8 -1
  35. package/hooks/ShopperGiftCertificates/queryKeyHelpers.d.ts +0 -5
  36. package/hooks/ShopperGiftCertificates/queryKeyHelpers.js +6 -2
  37. package/hooks/ShopperLogin/paramKeys.d.ts +7 -0
  38. package/hooks/ShopperLogin/paramKeys.js +21 -0
  39. package/hooks/ShopperLogin/query.js +24 -3
  40. package/hooks/ShopperLogin/queryKeyHelpers.d.ts +0 -5
  41. package/hooks/ShopperLogin/queryKeyHelpers.js +14 -6
  42. package/hooks/ShopperOrders/paramKeys.d.ts +7 -0
  43. package/hooks/ShopperOrders/paramKeys.js +21 -0
  44. package/hooks/ShopperOrders/query.js +24 -3
  45. package/hooks/ShopperOrders/queryKeyHelpers.d.ts +0 -5
  46. package/hooks/ShopperOrders/queryKeyHelpers.js +14 -6
  47. package/hooks/ShopperProducts/paramKeys.d.ts +8 -0
  48. package/hooks/ShopperProducts/paramKeys.js +23 -0
  49. package/hooks/ShopperProducts/query.js +30 -4
  50. package/hooks/ShopperProducts/queryKeyHelpers.d.ts +0 -5
  51. package/hooks/ShopperProducts/queryKeyHelpers.js +18 -8
  52. package/hooks/ShopperPromotions/paramKeys.d.ts +6 -0
  53. package/hooks/ShopperPromotions/paramKeys.js +19 -0
  54. package/hooks/ShopperPromotions/query.js +18 -2
  55. package/hooks/ShopperPromotions/queryKeyHelpers.d.ts +0 -5
  56. package/hooks/ShopperPromotions/queryKeyHelpers.js +10 -4
  57. package/hooks/ShopperSearch/paramKeys.d.ts +6 -0
  58. package/hooks/ShopperSearch/paramKeys.js +18 -0
  59. package/hooks/ShopperSearch/query.js +16 -2
  60. package/hooks/ShopperSearch/queryKeyHelpers.d.ts +0 -5
  61. package/hooks/ShopperSearch/queryKeyHelpers.js +10 -4
  62. package/hooks/ShopperSeo/index.d.ts +2 -0
  63. package/hooks/ShopperSeo/index.js +16 -0
  64. package/hooks/ShopperSeo/paramKeys.d.ts +5 -0
  65. package/hooks/ShopperSeo/paramKeys.js +17 -0
  66. package/hooks/ShopperSeo/query.d.ts +20 -0
  67. package/hooks/ShopperSeo/query.js +74 -0
  68. package/hooks/ShopperSeo/queryKeyHelpers.d.ts +24 -0
  69. package/hooks/ShopperSeo/queryKeyHelpers.js +28 -0
  70. package/hooks/index.d.ts +1 -0
  71. package/hooks/index.js +12 -0
  72. package/hooks/types.d.ts +2 -1
  73. package/hooks/useLocalStorage.d.ts +0 -1
  74. package/hooks/useLocalStorage.js +30 -20
  75. package/hooks/utils.d.ts +2 -0
  76. package/hooks/utils.js +11 -2
  77. package/package.json +5 -5
  78. package/provider.d.ts +12 -0
  79. package/provider.js +25 -8
package/CHANGELOG.md CHANGED
@@ -1,7 +1,15 @@
1
+ ## v1.4.0-dev (Jan 22, 2024)
2
+
3
+ - Add Support for SLAS private flow [#1722](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1722)
4
+ - Fix invalid query params warnings and allow custom query [#1655](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1655)
5
+ - Fix cannot read properties of undefined (reading 'unshift') [#1689](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1689)
6
+ - Add Shopper SEO hook [#1688](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1688)
7
+ - Update useLocalStorage implementation to be more responsive [#1703](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1703)
8
+ - Storefront Preview: avoid stale cached Commerce API responses, whenever the Shopper Context is set [#1701](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1701)
9
+
1
10
  ## v1.3.0 (Jan 19, 2024)
2
11
 
3
12
  - Add support for node 20 [#1612](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1612)
4
- - Fix bug when running in an iframe [#1629](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1629)
5
13
 
6
14
  ## v1.2.0 (Dec 08, 2023)
7
15
 
package/README.md CHANGED
@@ -112,6 +112,67 @@ _💡 This section assumes you have read and completed the [Authorization for Sh
112
112
 
113
113
  To help reduce boilerplate code for managing shopper authentication, by default, this library automatically initializes shopper session and manages the tokens for developers. Currently, the library supports the [Public Client login flow](https://developer.salesforce.com/docs/commerce/commerce-api/guide/slas-public-client.html).
114
114
 
115
+ Commerce-react-sdk supports both public and private flow of the [Authorization for Shopper APIs](https://developer.salesforce.com/docs/commerce/commerce-api/guide/authorization-for-shopper-apis.html) guide._
116
+ You can choose to use either public or private slas to login. By default, public flow is enabled.
117
+
118
+ #### How private SLAS works
119
+ This section assumes you read and understand how [private SLAS](https://developer.salesforce.com/docs/commerce/commerce-api/guide/slas-private-client.html) flow works
120
+
121
+ To enable private slas flow, you need to pass your secret into the CommercerProvider via clientSecret prop.
122
+ **Note** You should only use private slas if you know you can secure your secret since commercer-sdk-react runs isomorphically.
123
+
124
+ ```js
125
+ // app/components/_app-config/index.jsx
126
+
127
+ import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
128
+ import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
129
+
130
+ const AppConfig = ({children}) => {
131
+ return (
132
+ <CommerceApiProvider
133
+ clientId="12345678-1234-1234-1234-123412341234"
134
+ organizationId="f_ecom_aaaa_001"
135
+ proxy="localhost:3000/mobify/proxy/api"
136
+ redirectURI="localhost:3000/callback"
137
+ siteId="RefArch"
138
+ shortCode="12345678"
139
+ locale="en-US"
140
+ currency="USD"
141
+ clientSecret="<your-slas-private-secret>"
142
+ >
143
+ {children}
144
+ </CommerceApiProvider>
145
+ )
146
+ }
147
+ ```
148
+ #### Disable slas private warnings
149
+ By default, a warning as below will be displayed on client side to remind developers to always keep their secret safe and secured.
150
+ ```js
151
+ 'You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!'
152
+ ```
153
+ You can disable this warning by using CommerceProvider prop `silenceWarnings`
154
+
155
+ ```js
156
+ const AppConfig = ({children}) => {
157
+ return (
158
+ <CommerceApiProvider
159
+ clientId="12345678-1234-1234-1234-123412341234"
160
+ organizationId="f_ecom_aaaa_001"
161
+ proxy="localhost:3000/mobify/proxy/api"
162
+ redirectURI="localhost:3000/callback"
163
+ siteId="RefArch"
164
+ shortCode="12345678"
165
+ locale="en-US"
166
+ currency="USD"
167
+ clientSecret="<your-slas-private-secret>"
168
+ silenceWarnings={true}
169
+ >
170
+ {children}
171
+ </CommerceApiProvider>
172
+ )
173
+ }
174
+ ```
175
+
115
176
  ### Shopper Session Initialization
116
177
 
117
178
  On `CommerceApiProvider` mount, the provider initializes shopper session by initiating the SLAS **Public Client** login flow. The tokens are stored/managed/refreshed by the library.
@@ -121,7 +182,6 @@ On `CommerceApiProvider` mount, the provider initializes shopper session by init
121
182
  While the library is fetching/refreshing the access token, the network requests will queue up until there is a valid access token.
122
183
 
123
184
  ### Login helpers
124
-
125
185
  To leverage the managed shopper authentication feature, use the `useAuthHelper` hook for shopper login.
126
186
 
127
187
  Example:
@@ -144,6 +204,7 @@ const Example = () => {
144
204
 
145
205
  You have the option of handling shopper authentication externally, by providing a SLAS access token. This is useful if you plan on using this library in an application where the authentication mechanism is different.
146
206
 
207
+
147
208
  ```jsx
148
209
  const MyComponent = ({children}) => {
149
210
  return <CommerceApiProvider fetchedToken="xxxxxxxxxxxx">{children}</CommerceApiProvider>
package/auth/index.d.ts CHANGED
@@ -9,6 +9,9 @@ interface AuthConfig extends ApiClientConfigParams {
9
9
  fetchOptions?: ShopperLoginTypes.FetchOptions;
10
10
  fetchedToken?: string;
11
11
  OCAPISessionsURL?: string;
12
+ enablePWAKitPrivateClient?: boolean;
13
+ clientSecret?: string;
14
+ silenceWarnings?: boolean;
12
15
  }
13
16
  /**
14
17
  * The extended field is not from api response, we manually store the auth type,
@@ -21,7 +24,7 @@ export type AuthData = Prettify<RemoveStringIndex<TokenResponse> & {
21
24
  idp_access_token: string;
22
25
  }>;
23
26
  /** A shopper could be guest or registered, so we store the refresh tokens individually. */
24
- type AuthDataKeys = Exclude<keyof AuthData, 'refresh_token'> | 'refresh_token_guest' | 'refresh_token_registered' | 'refresh_token_guest_copy' | 'refresh_token_registered_copy';
27
+ type AuthDataKeys = Exclude<keyof AuthData, 'refresh_token'> | 'refresh_token_guest' | 'refresh_token_registered' | 'access_token_sfra';
25
28
  /**
26
29
  * This class is used to handle shopper authentication.
27
30
  * It is responsible for initializing shopper session, manage access
@@ -35,11 +38,11 @@ declare class Auth {
35
38
  private shopperCustomersClient;
36
39
  private redirectURI;
37
40
  private pendingToken;
38
- private REFRESH_TOKEN_EXPIRATION_DAYS_REGISTERED;
39
- private REFRESH_TOKEN_EXPIRATION_DAYS_GUEST;
40
41
  private stores;
41
42
  private fetchedToken;
42
43
  private OCAPISessionsURL;
44
+ private clientSecret;
45
+ private silenceWarnings;
43
46
  constructor(config: AuthConfig);
44
47
  get(name: AuthDataKeys): string;
45
48
  private set;
@@ -53,20 +56,21 @@ declare class Auth {
53
56
  */
54
57
  private isTokenExpired;
55
58
  /**
56
- * WARNING: This function is relevant to be used in Hybrid deployments only.
57
- * Compares the refresh_token keys for guest('cc-nx-g') and registered('cc-nx') login from the cookie received from SFRA with the copy stored in localstorage on PWA Kit
58
- * to determine if the login state of the shopper on SFRA site has changed. If the keys are different we return true considering the login state did change. If the keys are same,
59
- * we compare the values of the refresh_token to cover an edge case where the login state might have changed multiple times on SFRA and the eventual refresh_token key might be same
60
- * as that on PWA Kit which would incorrectly show both keys to be the same even though the sessions are different.
61
- * @returns {boolean} true if the keys do not match (login state changed), false otherwise.
62
- */
63
- private hasSFRAAuthStateChanged;
64
- /**
65
- * Used to validate JWT expiry and ensure auth state consistency with SFRA in a hybrid setup
66
- * @param token access_token received on SLAS authentication
67
- * @returns {boolean} true if JWT is valid; false otherwise
59
+ * Returns the SLAS access token or an empty string if the access token
60
+ * is not found in local store or if SFRA wants PWA to trigger refresh token login.
61
+ *
62
+ * On PWA-only sites, this returns the access token from local storage.
63
+ * On Hybrid sites, this checks whether SFRA has sent an auth token via cookies.
64
+ * Returns an access token from SFRA if it exist.
65
+ * If not, the access token from local store is returned.
66
+ *
67
+ * This is only used within this Auth module since other modules consider the access
68
+ * token from this.get('access_token') to be the source of truth.
69
+ *
70
+ * @returns {string} access token
68
71
  */
69
- private isTokenValidForHybrid;
72
+ private getAccessToken;
73
+ private clearSFRAAuthToken;
70
74
  /**
71
75
  * This method stores the TokenResponse object retrived from SLAS, and
72
76
  * store the data in storage.
@@ -80,6 +84,7 @@ declare class Auth {
80
84
  * @Internal
81
85
  */
82
86
  queueRequest(fn: () => Promise<TokenResponse>, isGuest: boolean): Promise<ShopperLoginTypes.TokenResponse>;
87
+ logWarning: (msg: string) => void;
83
88
  /**
84
89
  * The ready function returns a promise that resolves with valid ShopperLogin
85
90
  * token response.
package/auth/index.js CHANGED
@@ -8,6 +8,7 @@ var _commerceSdkIsomorphic = require("commerce-sdk-isomorphic");
8
8
  var _jwtDecode = require("jwt-decode");
9
9
  var _storage = require("./storage");
10
10
  var _utils = require("../utils");
11
+ var _constant = require("../constant");
11
12
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
12
13
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
13
14
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
@@ -87,27 +88,25 @@ const DATA_MAP = {
87
88
  storageType: 'local',
88
89
  key: 'refresh_token_expires_in'
89
90
  },
90
- // For Hybrid setups, we need a mechanism to inform PWA Kit whenever customer login state changes on SFRA.
91
- // So we maintain a copy of the refersh_tokens in the local storage which is compared to the actual refresh_token stored in cookie storage.
92
- // If the key or value of the refresh_token in local storage is different from the one in cookie storage, this indicates a change in customer auth state and we invalidate the access_token in PWA Kit.
93
- // This triggers a new fetch for access_token using the current refresh_token from cookie storage and makes sure customer auth state is always in sync between SFRA and PWA sites in a hybrid setup.
94
- refresh_token_guest_copy: {
95
- storageType: 'local',
96
- key: isParentTrusted ? 'cc-nx-g-iframe' : 'cc-nx-g',
97
- callback: store => {
98
- store.delete(isParentTrusted ? 'cc-nx-iframe' : 'cc-nx');
99
- }
100
- },
101
- refresh_token_registered_copy: {
102
- storageType: 'local',
103
- key: isParentTrusted ? 'cc-nx-iframe' : 'cc-nx',
104
- callback: store => {
105
- store.delete(isParentTrusted ? 'cc-nx-g-iframe' : 'cc-nx-g');
106
- }
107
- },
108
91
  customer_type: {
109
92
  storageType: 'local',
110
93
  key: 'customer_type'
94
+ },
95
+ /*
96
+ * For Hybrid setups, we need a mechanism to inform PWA Kit whenever customer login state changes on SFRA.
97
+ * We do this by having SFRA store the access token in cookies. If these cookies are present, PWA
98
+ * compares the access token from the cookie with the one in local store. If the tokens are different,
99
+ * discard the access token in local store and replace it with the access token from the cookie.
100
+ *
101
+ * ECOM has a 1200 character limit on the values of cookies. The access token easily exceeds this amount
102
+ * so it sends the access token in chunks across several cookies.
103
+ *
104
+ * The JWT tends to come in at around 2250 characters so there's usually
105
+ * both a cc-at and cc-at_2.
106
+ */
107
+ access_token_sfra: {
108
+ storageType: 'cookie',
109
+ key: 'cc-at'
111
110
  }
112
111
  };
113
112
 
@@ -120,11 +119,12 @@ const DATA_MAP = {
120
119
  * @Internal
121
120
  */
122
121
  class Auth {
123
- REFRESH_TOKEN_EXPIRATION_DAYS_REGISTERED = 90;
124
- REFRESH_TOKEN_EXPIRATION_DAYS_GUEST = 30;
125
122
  constructor(config) {
123
+ // Special endpoint for injecting SLAS private client secret
124
+ const baseUrl = config.proxy.split(`/mobify/proxy/api`)[0];
125
+ const privateClientEndpoint = `${baseUrl}/mobify/scapi/shopper/auth`;
126
126
  this.client = new _commerceSdkIsomorphic.ShopperLogin({
127
- proxy: config.proxy,
127
+ proxy: config.enablePWAKitPrivateClient ? privateClientEndpoint : config.proxy,
128
128
  parameters: {
129
129
  clientId: config.clientId,
130
130
  organizationId: config.organizationId,
@@ -158,6 +158,35 @@ class Auth {
158
158
  this.redirectURI = config.redirectURI;
159
159
  this.fetchedToken = config.fetchedToken || '';
160
160
  this.OCAPISessionsURL = config.OCAPISessionsURL || '';
161
+
162
+ /*
163
+ * There are 2 ways to enable SLAS private client mode.
164
+ * If enablePWAKitPrivateClient=true, we route SLAS calls to /mobify/scapi/shopper/auth
165
+ * and set an internal placeholder as the client secret. The proxy will override the placeholder
166
+ * with the actual client secret so any truthy value as the placeholder works here.
167
+ *
168
+ * If enablePWAKitPrivateClient=false and clientSecret is provided as a non-empty string,
169
+ * private client mode is enabled but we don't route calls to /mobify/scapi/shopper/auth
170
+ * This is how non-PWA Kit consumers of commerce-sdk-react can enable private client and set a secret
171
+ *
172
+ * If both enablePWAKitPrivateClient and clientSecret are truthy, enablePWAKitPrivateClient takes
173
+ * priority and we ignore whatever was set for clientSecret. This prints a warning about the clientSecret
174
+ * being ignored.
175
+ *
176
+ * If both enablePWAKitPrivateClient and clientSecret are falsey, we are in SLAS public client mode.
177
+ */
178
+ if (config.enablePWAKitPrivateClient && config.clientSecret) {
179
+ this.logWarning(_constant.SLAS_SECRET_OVERRIDE_MSG);
180
+ }
181
+ this.clientSecret = config.enablePWAKitPrivateClient ?
182
+ // PWA proxy is enabled, assume project is PWA and that the proxy will handle setting the secret
183
+ // We can pass any truthy value here to satisfy commerce-sdk-isomorphic requirements
184
+ _constant.SLAS_SECRET_PLACEHOLDER :
185
+ // We think there are users of Commerce SDK React and Commerce SDK isomorphic outside of PWA
186
+ // For these users to use a private client, they must have some way to set a client secret
187
+ // PWA users should not need to touch this.
188
+ config.clientSecret || '';
189
+ this.silenceWarnings = config.silenceWarnings || false;
161
190
  }
162
191
  get(name) {
163
192
  const {
@@ -223,29 +252,56 @@ class Auth {
223
252
  }
224
253
 
225
254
  /**
226
- * WARNING: This function is relevant to be used in Hybrid deployments only.
227
- * Compares the refresh_token keys for guest('cc-nx-g') and registered('cc-nx') login from the cookie received from SFRA with the copy stored in localstorage on PWA Kit
228
- * to determine if the login state of the shopper on SFRA site has changed. If the keys are different we return true considering the login state did change. If the keys are same,
229
- * we compare the values of the refresh_token to cover an edge case where the login state might have changed multiple times on SFRA and the eventual refresh_token key might be same
230
- * as that on PWA Kit which would incorrectly show both keys to be the same even though the sessions are different.
231
- * @returns {boolean} true if the keys do not match (login state changed), false otherwise.
255
+ * Returns the SLAS access token or an empty string if the access token
256
+ * is not found in local store or if SFRA wants PWA to trigger refresh token login.
257
+ *
258
+ * On PWA-only sites, this returns the access token from local storage.
259
+ * On Hybrid sites, this checks whether SFRA has sent an auth token via cookies.
260
+ * Returns an access token from SFRA if it exist.
261
+ * If not, the access token from local store is returned.
262
+ *
263
+ * This is only used within this Auth module since other modules consider the access
264
+ * token from this.get('access_token') to be the source of truth.
265
+ *
266
+ * @returns {string} access token
232
267
  */
233
- hasSFRAAuthStateChanged() {
234
- const refreshTokenKey = this.get('refresh_token_registered') && 'refresh_token_registered' || 'refresh_token_guest';
235
- const refreshTokenCopyKey = this.get('refresh_token_registered_copy') && 'refresh_token_registered_copy' || 'refresh_token_guest_copy';
236
- if (DATA_MAP[refreshTokenKey].key !== DATA_MAP[refreshTokenCopyKey].key) {
237
- return true;
268
+ getAccessToken() {
269
+ let accessToken = this.get('access_token');
270
+ const sfraAuthToken = this.get('access_token_sfra');
271
+ if (sfraAuthToken) {
272
+ /*
273
+ * If SFRA sends 'refresh', we return an empty token here so PWA can trigger a login refresh
274
+ * This key is used when logout is triggered in SFRA but the redirect after logout
275
+ * sends the user to PWA.
276
+ */
277
+ if (sfraAuthToken === 'refresh') {
278
+ this.set('access_token', '');
279
+ this.clearSFRAAuthToken();
280
+ return '';
281
+ }
282
+ const {
283
+ isGuest,
284
+ customerId,
285
+ usid
286
+ } = this.parseSlasJWT(sfraAuthToken);
287
+ this.set('access_token', sfraAuthToken);
288
+ this.set('customer_id', customerId);
289
+ this.set('usid', usid);
290
+ this.set('customer_type', isGuest ? 'guest' : 'registered');
291
+ accessToken = sfraAuthToken;
292
+ // SFRA -> PWA access token cookie handoff is succesful so we clear the SFRA made cookies.
293
+ // We don't want these cookies to persist and continue overriding what is in local store.
294
+ this.clearSFRAAuthToken();
238
295
  }
239
- return this.get(refreshTokenKey) !== this.get(refreshTokenCopyKey);
296
+ return accessToken;
240
297
  }
241
-
242
- /**
243
- * Used to validate JWT expiry and ensure auth state consistency with SFRA in a hybrid setup
244
- * @param token access_token received on SLAS authentication
245
- * @returns {boolean} true if JWT is valid; false otherwise
246
- */
247
- isTokenValidForHybrid(token) {
248
- return !this.isTokenExpired(token) && !this.hasSFRAAuthStateChanged();
298
+ clearSFRAAuthToken() {
299
+ const {
300
+ key,
301
+ storageType
302
+ } = DATA_MAP['access_token_sfra'];
303
+ const store = this.stores[storageType];
304
+ store.delete(key);
249
305
  }
250
306
 
251
307
  /**
@@ -263,13 +319,8 @@ class Auth {
263
319
  this.set('usid', res.usid);
264
320
  this.set('customer_type', isGuest ? 'guest' : 'registered');
265
321
  const refreshTokenKey = isGuest ? 'refresh_token_guest' : 'refresh_token_registered';
266
- const refreshTokenCopyKey = isGuest ? 'refresh_token_guest_copy' : 'refresh_token_registered_copy';
267
- const refreshTokenExpiry = isGuest ? this.REFRESH_TOKEN_EXPIRATION_DAYS_GUEST : this.REFRESH_TOKEN_EXPIRATION_DAYS_REGISTERED;
268
322
  this.set(refreshTokenKey, res.refresh_token, {
269
- expires: refreshTokenExpiry
270
- });
271
- this.set(refreshTokenCopyKey, res.refresh_token, {
272
- expires: refreshTokenExpiry
323
+ expires: res.refresh_token_expires_in
273
324
  });
274
325
  }
275
326
 
@@ -300,6 +351,11 @@ class Auth {
300
351
  return yield _this.pendingToken;
301
352
  })();
302
353
  }
354
+ logWarning = msg => {
355
+ if (!this.silenceWarnings) {
356
+ console.warn(msg);
357
+ }
358
+ };
303
359
 
304
360
  /**
305
361
  * The ready function returns a promise that resolves with valid ShopperLogin
@@ -330,8 +386,8 @@ class Auth {
330
386
  if (_this2.pendingToken) {
331
387
  return yield _this2.pendingToken;
332
388
  }
333
- const accessToken = _this2.get('access_token');
334
- if (accessToken && _this2.isTokenValidForHybrid(accessToken)) {
389
+ const accessToken = _this2.getAccessToken();
390
+ if (accessToken && !_this2.isTokenExpired(accessToken)) {
335
391
  return _this2.data;
336
392
  }
337
393
  const refreshTokenRegistered = _this2.get('refresh_token_registered');
@@ -341,6 +397,8 @@ class Auth {
341
397
  try {
342
398
  return yield _this2.queueRequest(() => _commerceSdkIsomorphic.helpers.refreshAccessToken(_this2.client, {
343
399
  refreshToken
400
+ }, {
401
+ clientSecret: _this2.clientSecret
344
402
  }), !!refreshTokenGuest);
345
403
  } catch (error) {
346
404
  // If the refresh token is invalid, we need to re-login the user
@@ -356,9 +414,7 @@ class Auth {
356
414
  }
357
415
  }
358
416
  }
359
- return yield _this2.queueRequest(() => _commerceSdkIsomorphic.helpers.loginGuestUser(_this2.client, {
360
- redirectURI: _this2.redirectURI
361
- }), true);
417
+ return _this2.loginGuestUser();
362
418
  })();
363
419
  }
364
420
 
@@ -382,14 +438,23 @@ class Auth {
382
438
  loginGuestUser() {
383
439
  var _this4 = this;
384
440
  return _asyncToGenerator(function* () {
385
- const redirectURI = _this4.redirectURI;
441
+ if (_this4.clientSecret && (0, _utils.onClient)() && _this4.clientSecret !== _constant.SLAS_SECRET_PLACEHOLDER) {
442
+ _this4.logWarning(_constant.SLAS_SECRET_WARNING_MSG);
443
+ }
386
444
  const usid = _this4.get('usid');
387
445
  const isGuest = true;
388
- return yield _this4.queueRequest(() => _commerceSdkIsomorphic.helpers.loginGuestUser(_this4.client, _objectSpread({
389
- redirectURI
446
+ const guestPrivateArgs = [_this4.client, _objectSpread({}, usid && {
447
+ usid
448
+ }), {
449
+ clientSecret: _this4.clientSecret
450
+ }];
451
+ const guestPublicArgs = [_this4.client, _objectSpread({
452
+ redirectURI: _this4.redirectURI
390
453
  }, usid && {
391
454
  usid
392
- })), isGuest);
455
+ })];
456
+ const callback = _this4.clientSecret ? () => _commerceSdkIsomorphic.helpers.loginGuestUserPrivate(...guestPrivateArgs) : () => _commerceSdkIsomorphic.helpers.loginGuestUser(...guestPublicArgs);
457
+ return yield _this4.queueRequest(callback, isGuest);
393
458
  })();
394
459
  }
395
460
 
@@ -434,10 +499,15 @@ class Auth {
434
499
  loginRegisteredUserB2C(credentials) {
435
500
  var _this6 = this;
436
501
  return _asyncToGenerator(function* () {
502
+ if (_this6.clientSecret && (0, _utils.onClient)() && _this6.clientSecret !== _constant.SLAS_SECRET_PLACEHOLDER) {
503
+ _this6.logWarning(_constant.SLAS_SECRET_WARNING_MSG);
504
+ }
437
505
  const redirectURI = _this6.redirectURI;
438
506
  const usid = _this6.get('usid');
439
507
  const isGuest = false;
440
- const token = yield _commerceSdkIsomorphic.helpers.loginRegisteredUserB2C(_this6.client, credentials, _objectSpread({
508
+ const token = yield _commerceSdkIsomorphic.helpers.loginRegisteredUserB2C(_this6.client, _objectSpread(_objectSpread({}, credentials), {}, {
509
+ clientSecret: _this6.clientSecret
510
+ }), _objectSpread({
441
511
  redirectURI
442
512
  }, usid && {
443
513
  usid
@@ -38,11 +38,35 @@ class CookieStorage extends _base.BaseStorage {
38
38
  }
39
39
  get(key) {
40
40
  const suffixedKey = this.getSuffixedKey(key);
41
- return _jsCookie.default.get(suffixedKey) || '';
41
+ let value = _jsCookie.default.get(suffixedKey) || '';
42
+ if (value) {
43
+ // Some values, like the access token, may be split
44
+ // across multiple keys to fit under ECOM cookie size
45
+ // thresholds. We check for and append additional chunks here.
46
+ let chunk = 2;
47
+ let additionalPart = _jsCookie.default.get(`${suffixedKey}_${chunk}`);
48
+ while (additionalPart) {
49
+ value = value.concat(additionalPart);
50
+ chunk++;
51
+ additionalPart = _jsCookie.default.get(`${suffixedKey}_${chunk}`) || '';
52
+ }
53
+ }
54
+ return value;
42
55
  }
43
56
  delete(key, options) {
44
57
  const suffixedKey = this.getSuffixedKey(key);
45
58
  _jsCookie.default.remove(suffixedKey, _objectSpread(_objectSpread({}, (0, _utils.getDefaultCookieAttributes)()), options));
59
+
60
+ // Some values, like the access token, may be split
61
+ // across multiple keys to fit under ECOM cookie size
62
+ // thresholds. We check for and delete additional chunks here.
63
+ let chunk = 2;
64
+ let additionalPart = _jsCookie.default.get(`${suffixedKey}_${chunk}`);
65
+ while (additionalPart) {
66
+ _jsCookie.default.remove(`${suffixedKey}_${chunk}`, _objectSpread(_objectSpread({}, (0, _utils.getDefaultCookieAttributes)()), options));
67
+ chunk++;
68
+ additionalPart = _jsCookie.default.get(`${suffixedKey}_${chunk}`) || '';
69
+ }
46
70
  }
47
71
  }
48
72
  exports.CookieStorage = CookieStorage;
@@ -9,6 +9,7 @@ var _propTypes = _interopRequireDefault(require("prop-types"));
9
9
  var _reactHelmet = require("react-helmet");
10
10
  var _utils = require("./utils");
11
11
  var _reactRouterDom = require("react-router-dom");
12
+ var _hooks = require("../../hooks");
12
13
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
14
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
14
15
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
@@ -36,6 +37,7 @@ const StorefrontPreview = ({
36
37
  }) => {
37
38
  const history = (0, _reactRouterDom.useHistory)();
38
39
  const isHostTrusted = (0, _utils.detectStorefrontPreview)();
40
+ const apiClients = (0, _hooks.useCommerceApi)();
39
41
  (0, _react.useEffect)(() => {
40
42
  if (enabled && isHostTrusted) {
41
43
  window.STOREFRONT_PREVIEW = _objectSpread(_objectSpread({}, window.STOREFRONT_PREVIEW), {}, {
@@ -47,6 +49,24 @@ const StorefrontPreview = ({
47
49
  });
48
50
  }
49
51
  }, [enabled, getToken, onContextChange]);
52
+ (0, _react.useEffect)(() => {
53
+ if (enabled && isHostTrusted) {
54
+ // In Storefront Preview mode, add cache breaker for all SCAPI's requests.
55
+ // Otherwise, it's possible to get stale responses after the Shopper Context is set.
56
+ // (i.e. in this case, we optimize for accurate data, rather than performance/caching)
57
+ (0, _utils.proxyRequests)(apiClients, {
58
+ apply(target, thisArg, argumentsList) {
59
+ var _argumentsList$;
60
+ argumentsList[0] = _objectSpread(_objectSpread({}, argumentsList[0]), {}, {
61
+ parameters: _objectSpread(_objectSpread({}, (_argumentsList$ = argumentsList[0]) === null || _argumentsList$ === void 0 ? void 0 : _argumentsList$.parameters), {}, {
62
+ c_cache_breaker: Date.now()
63
+ })
64
+ });
65
+ return target.call(thisArg, ...argumentsList);
66
+ }
67
+ });
68
+ }
69
+ }, [apiClients, enabled]);
50
70
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, enabled && isHostTrusted && /*#__PURE__*/_react.default.createElement(_reactHelmet.Helmet, null, /*#__PURE__*/_react.default.createElement("script", {
51
71
  id: "storefront_preview",
52
72
  src: (0, _utils.getClientScript)(),
@@ -1,3 +1,4 @@
1
+ import { ApiClients } from '../../hooks/types';
1
2
  /** Detects whether the storefront is running in an iframe as part of Storefront Preview.
2
3
  * @private
3
4
  */
@@ -18,4 +19,9 @@ export declare const CustomPropTypes: {
18
19
  */
19
20
  requiredFunctionWhenEnabled: (props: any, propName: any, componentName: any) => Error | undefined;
20
21
  };
22
+ /**
23
+ * Via the built-in Proxy object, modify the behaviour of each request for the given SCAPI clients
24
+ * @private
25
+ */
26
+ export declare const proxyRequests: (clients: ApiClients, handlers: ProxyHandler<any>) => void;
21
27
  //# sourceMappingURL=utils.d.ts.map
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.getClientScript = exports.detectStorefrontPreview = exports.CustomPropTypes = void 0;
6
+ exports.proxyRequests = exports.getClientScript = exports.detectStorefrontPreview = exports.CustomPropTypes = void 0;
7
7
  var _utils = require("../../utils");
8
8
  /*
9
9
  * Copyright (c) 2023, Salesforce, Inc.
@@ -35,10 +35,10 @@ const CustomPropTypes = exports.CustomPropTypes = {
35
35
  /**
36
36
  * This custom PropType ensures that the prop is only required when the known prop
37
37
  * "enabled" is set to "true".
38
- *
39
- * @param props
40
- * @param propName
41
- * @param componentName
38
+ *
39
+ * @param props
40
+ * @param propName
41
+ * @param componentName
42
42
  * @returns
43
43
  */
44
44
  requiredFunctionWhenEnabled: (props, propName, componentName) => {
@@ -46,4 +46,18 @@ const CustomPropTypes = exports.CustomPropTypes = {
46
46
  return new Error(`${String(propName)} is a required function for ${String(componentName)} when enabled is true`);
47
47
  }
48
48
  }
49
- };
49
+ };
50
+
51
+ /**
52
+ * Via the built-in Proxy object, modify the behaviour of each request for the given SCAPI clients
53
+ * @private
54
+ */
55
+ const proxyRequests = (clients, handlers) => {
56
+ Object.values(clients).forEach(client => {
57
+ const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(client));
58
+ methods.forEach(method => {
59
+ client[method] = new Proxy(client[method], handlers);
60
+ });
61
+ });
62
+ };
63
+ exports.proxyRequests = proxyRequests;
package/constant.d.ts CHANGED
@@ -2,4 +2,7 @@
2
2
  * This list contains domains that can host code in iframe
3
3
  */
4
4
  export declare const IFRAME_HOST_ALLOW_LIST: readonly string[];
5
+ export declare const SLAS_SECRET_WARNING_MSG = "You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!";
6
+ export declare const SLAS_SECRET_PLACEHOLDER = "_PLACEHOLDER_PROXY-PWA_KIT_SLAS_CLIENT_SECRET";
7
+ export declare const SLAS_SECRET_OVERRIDE_MSG = "You have enabled PWA Kit Private Client mode which gets the SLAS secret from your environment variable. The SLAS secret you have set in the Auth provider will be ignored.";
5
8
  //# sourceMappingURL=constant.d.ts.map
package/constant.js CHANGED
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.IFRAME_HOST_ALLOW_LIST = void 0;
6
+ exports.SLAS_SECRET_WARNING_MSG = exports.SLAS_SECRET_PLACEHOLDER = exports.SLAS_SECRET_OVERRIDE_MSG = exports.IFRAME_HOST_ALLOW_LIST = void 0;
7
7
  /*
8
8
  * Copyright (c) 2023, Salesforce, Inc.
9
9
  * All rights reserved.
@@ -14,4 +14,7 @@ exports.IFRAME_HOST_ALLOW_LIST = void 0;
14
14
  /**
15
15
  * This list contains domains that can host code in iframe
16
16
  */
17
- const IFRAME_HOST_ALLOW_LIST = exports.IFRAME_HOST_ALLOW_LIST = Object.freeze(['https://runtime.commercecloud.com', 'https://runtime-admin-staging.mobify-storefront.com', 'https://runtime-admin-preview.mobify-storefront.com']);
17
+ const IFRAME_HOST_ALLOW_LIST = exports.IFRAME_HOST_ALLOW_LIST = Object.freeze(['https://runtime.commercecloud.com', 'https://runtime-admin-staging.mobify-storefront.com', 'https://runtime-admin-preview.mobify-storefront.com']);
18
+ const SLAS_SECRET_WARNING_MSG = exports.SLAS_SECRET_WARNING_MSG = 'You are potentially exposing SLAS secret on browser. Make sure to keep it safe and secure!';
19
+ const SLAS_SECRET_PLACEHOLDER = exports.SLAS_SECRET_PLACEHOLDER = '_PLACEHOLDER_PROXY-PWA_KIT_SLAS_CLIENT_SECRET';
20
+ const SLAS_SECRET_OVERRIDE_MSG = exports.SLAS_SECRET_OVERRIDE_MSG = 'You have enabled PWA Kit Private Client mode which gets the SLAS secret from your environment variable. The SLAS secret you have set in the Auth provider will be ignored.';
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ getBasket: readonly ["organizationId", "basketId", "siteId", "locale"];
3
+ getPaymentMethodsForBasket: readonly ["organizationId", "basketId", "siteId", "locale"];
4
+ getPriceBooksForBasket: readonly ["organizationId", "basketId", "siteId"];
5
+ getShippingMethodsForShipment: readonly ["organizationId", "basketId", "shipmentId", "siteId", "locale"];
6
+ getTaxesFromBasket: readonly ["organizationId", "basketId", "siteId"];
7
+ };
8
+ export default _default;
9
+ //# sourceMappingURL=paramKeys.d.ts.map