@magento/peregrine 15.6.2 → 15.7.2-alpha3

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.
@@ -1,14 +1,28 @@
1
1
  import { setContext } from '@apollo/client/link/context';
2
- import { BrowserPersistence } from '@magento/peregrine/lib/util';
2
+ import {
3
+ BrowserPersistence,
4
+ getTokenFromCookie,
5
+ shouldPreferCookies
6
+ } from '@magento/peregrine/lib/util';
3
7
 
4
8
  const storage = new BrowserPersistence();
5
9
 
6
10
  export default function createAuthLink() {
7
11
  return setContext((_, { headers }) => {
8
- // get the authentication token from local storage if it exists.
9
- const token = storage.getItem('signin_token');
12
+ let token = null;
10
13
 
11
- // return the headers to the context so httpLink can read them
14
+ // In standalone PWA mode (e.g., iOS home screen app), prefer cookies
15
+ // because localStorage is not shared between browser and PWA
16
+ if (shouldPreferCookies()) {
17
+ token = getTokenFromCookie();
18
+ }
19
+
20
+ // Fallback to localStorage (existing behavior)
21
+ if (!token) {
22
+ token = storage.getItem('signin_token');
23
+ }
24
+
25
+ // Return the headers to the context so httpLink can read them
12
26
  return {
13
27
  headers: {
14
28
  ...headers,
@@ -28,7 +28,14 @@ export const customFetchToShrinkQuery = (uri, options) => {
28
28
 
29
29
  const resource = options.method === 'GET' ? shrinkQuery(uri) : uri;
30
30
 
31
- return globalThis.fetch(resource, options);
31
+ // Include credentials to enable cookie-based authentication
32
+ // This allows session sharing between browser and PWA on iOS
33
+ const optionsWithCredentials = {
34
+ ...options,
35
+ credentials: 'include'
36
+ };
37
+
38
+ return globalThis.fetch(resource, optionsWithCredentials);
32
39
  };
33
40
 
34
41
  const getLinks = apiBase => {
@@ -22,6 +22,9 @@ const typePolicies = {
22
22
  },
23
23
  customerWishlistProducts: {
24
24
  read: existing => existing || []
25
+ },
26
+ products: {
27
+ keyArgs: ['filter', 'search', 'pageSize', 'currentPage', 'sort']
25
28
  }
26
29
  }
27
30
  },
@@ -319,7 +322,25 @@ const typePolicies = {
319
322
  keyFields: ['uid']
320
323
  },
321
324
  ConfigurableProduct: {
322
- keyFields: ['uid']
325
+ keyFields: ['uid'],
326
+ fields: {
327
+ variants: {
328
+ merge(existing, incoming) {
329
+ if (existing && existing.length > 0) {
330
+ return existing;
331
+ }
332
+ return incoming || existing;
333
+ }
334
+ },
335
+ configurable_options: {
336
+ merge(existing, incoming) {
337
+ if (existing && existing.length > 0) {
338
+ return existing;
339
+ }
340
+ return incoming || existing;
341
+ }
342
+ }
343
+ }
323
344
  },
324
345
  BundleProduct: {
325
346
  keyFields: ['uid']
@@ -1,4 +1,10 @@
1
- import React, { createContext, useContext, useMemo, useCallback } from 'react';
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useMemo,
5
+ useCallback,
6
+ useEffect
7
+ } from 'react';
2
8
  import { connect } from 'react-redux';
3
9
  import actions from '../store/actions/cart/actions';
4
10
  import * as asyncActions from '../store/actions/cart/asyncActions';
@@ -17,6 +23,16 @@ const getTotalQuantity = items =>
17
23
  const CartContextProvider = props => {
18
24
  const { actions, asyncActions, cartState, children } = props;
19
25
 
26
+ useEffect(() => {
27
+ const storage = new BrowserPersistence();
28
+ const cartId = storage.getItem('cartId');
29
+
30
+ if (cartId && (!cartState || cartState.cartId !== cartId)) {
31
+ asyncActions.getCartDetails(cartId);
32
+ }
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ }, []);
35
+
20
36
  // Make deeply nested details easier to retrieve and provide empty defaults
21
37
  const derivedDetails = useMemo(() => {
22
38
  if (isCartEmpty(cartState)) {
@@ -5,6 +5,7 @@ import actions from '../store/actions/user/actions';
5
5
  import * as asyncActions from '../store/actions/user/asyncActions';
6
6
  import bindActionCreators from '../util/bindActionCreators';
7
7
  import BrowserPersistence from '../util/simplePersistence';
8
+ import { getTokenFromCookie, shouldPreferCookies } from '../util/cookieHelper';
8
9
 
9
10
  const UserContext = createContext();
10
11
 
@@ -25,20 +26,35 @@ const UserContextProvider = props => {
25
26
  ]);
26
27
 
27
28
  useEffect(() => {
28
- // check if the user's token is not expired
29
+ // Check for authentication tokens (localStorage or cookie)
29
30
  const storage = new BrowserPersistence();
30
- const item = storage.getRawItem('signin_token');
31
+ let hasValidToken = false;
31
32
 
33
+ // First, check localStorage token (existing behavior)
34
+ const item = storage.getRawItem('signin_token');
32
35
  if (item) {
33
36
  const { ttl, timeStored } = JSON.parse(item);
34
37
  const now = Date.now();
35
38
 
36
- // if the token's TTYL has expired, we need to sign out
39
+ // if the token's TTL has expired, we need to sign out
37
40
  if (ttl && now - timeStored > ttl * 1000) {
38
41
  asyncActions.signOut();
42
+ return;
43
+ }
44
+ hasValidToken = true;
45
+ }
46
+
47
+ // Check for cookie token (for PWA session sharing)
48
+ // If we're in standalone PWA mode and have a cookie token but no localStorage token
49
+ if (!hasValidToken && shouldPreferCookies()) {
50
+ const cookieToken = getTokenFromCookie();
51
+ if (cookieToken && userState && !userState.isSignedIn) {
52
+ // Set the token in Redux so the app knows user is authenticated
53
+ actions.setToken(cookieToken);
54
+ hasValidToken = true;
39
55
  }
40
56
  }
41
- }, [asyncActions]);
57
+ }, [asyncActions, actions, userState]);
42
58
 
43
59
  return (
44
60
  <UserContext.Provider value={contextValue}>
@@ -86,6 +86,21 @@ export const clearToken = () =>
86
86
  // Clear token from local storage
87
87
  storage.removeItem('signin_token');
88
88
 
89
+ // Clear cookie for session sharing
90
+ if (
91
+ typeof document !== 'undefined' &&
92
+ typeof globalThis.location !== 'undefined'
93
+ ) {
94
+ try {
95
+ const hostname = globalThis.location.hostname;
96
+ // Clear with explicit domain (iOS compatibility)
97
+ document.cookie = `customer_token=; path=/; domain=${hostname}; max-age=0; secure; samesite=none`;
98
+ document.cookie = `customer_token=; path=/; domain=${hostname}; max-age=0; secure`;
99
+ } catch (error) {
100
+ // Silently fail if cookies are not available
101
+ }
102
+ }
103
+
89
104
  // Remove from store
90
105
  dispatch(actions.clearToken());
91
106
  };
@@ -268,7 +268,7 @@ export const useProductFullDetail = props => {
268
268
  const { data: storeConfigData } = useQuery(
269
269
  operations.getWishlistConfigQuery,
270
270
  {
271
- fetchPolicy: 'cache-and-network'
271
+ fetchPolicy: 'cache-first'
272
272
  }
273
273
  );
274
274
 
@@ -7,6 +7,8 @@ export const CategoryFragment = gql`
7
7
  meta_title
8
8
  meta_keywords
9
9
  meta_description
10
+ url_path
11
+ url_key
10
12
  }
11
13
  `;
12
14
 
@@ -49,6 +49,7 @@ export const useCategory = props => {
49
49
  nextFetchPolicy: 'cache-first'
50
50
  });
51
51
  const pageSize = pageSizeData && pageSizeData.storeConfig.grid_per_page;
52
+ const storeConfig = pageSizeData && pageSizeData.storeConfig;
52
53
 
53
54
  const [paginationValues, paginationApi] = usePagination();
54
55
  const { currentPage, totalPages } = paginationValues;
@@ -222,6 +223,7 @@ export const useCategory = props => {
222
223
  pageControl,
223
224
  sortProps,
224
225
  pageSize,
225
- categoryNotFound
226
+ categoryNotFound,
227
+ storeConfig
226
228
  };
227
229
  };
@@ -8,6 +8,7 @@ export const GET_STORE_CONFIG_DATA = gql`
8
8
  storeConfig {
9
9
  store_code
10
10
  product_url_suffix
11
+ product_canonical_tag
11
12
  }
12
13
  }
13
14
  `;
@@ -37,17 +37,20 @@ export const useProduct = props => {
37
37
  ] = useAppContext();
38
38
 
39
39
  const { data: storeConfigData } = useQuery(getStoreConfigData, {
40
- fetchPolicy: 'cache-and-network',
41
- nextFetchPolicy: 'cache-first'
40
+ fetchPolicy: 'cache-first'
42
41
  });
43
42
 
44
- const slug = pathname.split('/').pop();
45
- const productUrlSuffix = storeConfigData?.storeConfig?.product_url_suffix;
46
- const urlKey = productUrlSuffix ? slug.replace(productUrlSuffix, '') : slug;
43
+ const urlKey = useMemo(() => {
44
+ const slug = pathname.split('/').pop();
45
+ const productUrlSuffix =
46
+ storeConfigData?.storeConfig?.product_url_suffix;
47
+ return productUrlSuffix ? slug.replace(productUrlSuffix, '') : slug;
48
+ }, [pathname, storeConfigData?.storeConfig?.product_url_suffix]);
47
49
 
48
50
  const { error, loading, data } = useQuery(getProductDetailQuery, {
49
- fetchPolicy: 'cache-and-network',
50
- nextFetchPolicy: 'cache-first',
51
+ fetchPolicy: 'cache-first',
52
+ nextFetchPolicy: 'cache-only',
53
+ returnPartialData: true,
51
54
  skip: !storeConfigData,
52
55
  variables: {
53
56
  urlKey
@@ -112,6 +115,7 @@ export const useProduct = props => {
112
115
  return {
113
116
  error,
114
117
  loading,
115
- product
118
+ product,
119
+ storeConfig: storeConfigData?.storeConfig
116
120
  };
117
121
  };
@@ -126,6 +126,32 @@ export const useSignIn = props => {
126
126
  ? setToken(token, customerAccessTokenLifetime)
127
127
  : setToken(token));
128
128
 
129
+ // Set cookie for iOS PWA session sharing
130
+ // This enables authentication to persist when user adds web app to home screen
131
+ if (
132
+ typeof document !== 'undefined' &&
133
+ typeof window !== 'undefined'
134
+ ) {
135
+ try {
136
+ const maxAge = customerAccessTokenLifetime
137
+ ? customerAccessTokenLifetime * 3600
138
+ : 3600;
139
+
140
+ const hostname = window.location.hostname;
141
+
142
+ // Set multiple cookie variations for iOS compatibility
143
+ // iOS has quirks with SameSite=None on some versions
144
+ // Primary: With explicit domain and SameSite=None (iOS 13+)
145
+ document.cookie = `customer_token=${token}; path=/; domain=${hostname}; max-age=${maxAge}; secure; samesite=none`;
146
+
147
+ // Fallback: Without SameSite (for older iOS versions)
148
+ document.cookie = `customer_token=${token}; path=/; domain=${hostname}; max-age=${maxAge}; secure`;
149
+ } catch (error) {
150
+ // Silently fail if cookies are blocked
151
+ // Authentication will still work via localStorage
152
+ }
153
+ }
154
+
129
155
  // Clear all cart/customer data from cache and redux.
130
156
  await apolloClient.clearCacheData(apolloClient, 'cart');
131
157
  await apolloClient.clearCacheData(apolloClient, 'customer');
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Cookie utility helper for session sharing between browser and PWA standalone mode.
3
+ * This enables iOS PWA to maintain authentication state when added to home screen.
4
+ */
5
+
6
+ /**
7
+ * Get a cookie value by name
8
+ * @param {string} name - Cookie name
9
+ * @returns {string|null} Cookie value or null if not found
10
+ */
11
+ export const getCookie = name => {
12
+ if (typeof document === 'undefined') {
13
+ return null;
14
+ }
15
+
16
+ const nameEQ = name + '=';
17
+ const cookies = document.cookie.split(';');
18
+
19
+ for (let i = 0; i < cookies.length; i++) {
20
+ let cookie = cookies[i];
21
+ while (cookie.charAt(0) === ' ') {
22
+ cookie = cookie.substring(1, cookie.length);
23
+ }
24
+ if (cookie.indexOf(nameEQ) === 0) {
25
+ return cookie.substring(nameEQ.length, cookie.length);
26
+ }
27
+ }
28
+ return null;
29
+ };
30
+
31
+ /**
32
+ * Check if cookies are available and working
33
+ * @returns {boolean} True if cookies are supported
34
+ */
35
+ export const areCookiesAvailable = () => {
36
+ if (typeof document === 'undefined') {
37
+ return false;
38
+ }
39
+
40
+ try {
41
+ // Try to set a test cookie
42
+ document.cookie = 'cookietest=1; path=/';
43
+ const cookiesEnabled = document.cookie.indexOf('cookietest=') !== -1;
44
+ // Delete test cookie
45
+ document.cookie =
46
+ 'cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/';
47
+ return cookiesEnabled;
48
+ } catch (e) {
49
+ return false;
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Detect if app is running in standalone mode (installed PWA)
55
+ * @returns {boolean} True if running as standalone PWA
56
+ */
57
+ export const isStandalonePWA = () => {
58
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
59
+ return false;
60
+ }
61
+
62
+ // Check for iOS standalone mode
63
+ const isIOSStandalone =
64
+ 'standalone' in window.navigator && window.navigator.standalone;
65
+
66
+ // Check for Android/Desktop PWA
67
+ const isDisplayStandalone = window.matchMedia('(display-mode: standalone)')
68
+ .matches;
69
+
70
+ return isIOSStandalone || isDisplayStandalone;
71
+ };
72
+
73
+ /**
74
+ * Get authentication token from cookie (for backend cookie-based auth)
75
+ * @returns {string|null} Token from cookie or null
76
+ */
77
+ export const getTokenFromCookie = () => {
78
+ // Check for common Magento/PHP session cookie names
79
+ // You may need to adjust these based on your backend configuration
80
+ const possibleCookieNames = [
81
+ 'auth_token',
82
+ 'customer_token',
83
+ 'PHPSESSID',
84
+ 'magento_customer_token'
85
+ ];
86
+
87
+ for (const cookieName of possibleCookieNames) {
88
+ const value = getCookie(cookieName);
89
+ if (value) {
90
+ return value;
91
+ }
92
+ }
93
+
94
+ return null;
95
+ };
96
+
97
+ /**
98
+ * Check if we should prefer cookies over localStorage
99
+ * This is true in standalone PWA mode where localStorage might not be shared
100
+ * @returns {boolean}
101
+ */
102
+ export const shouldPreferCookies = () => {
103
+ return isStandalonePWA() && areCookiesAvailable();
104
+ };
package/lib/util/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { default as BrowserPersistence } from './simplePersistence';
2
+ export * from './cookieHelper';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magento/peregrine",
3
- "version": "15.6.2",
3
+ "version": "15.7.2-alpha3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },