@magento/peregrine 15.6.2-beta3 → 15.7.2-alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Apollo/links/authLink.js +18 -4
- package/lib/Apollo/links/index.js +8 -1
- package/lib/Apollo/policies/index.js +22 -1
- package/lib/context/cart.js +17 -1
- package/lib/context/user.js +20 -4
- package/lib/store/actions/user/asyncActions.js +15 -0
- package/lib/talons/ProductFullDetail/useProductFullDetail.js +1 -1
- package/lib/talons/RootComponents/Product/useProduct.js +10 -7
- package/lib/talons/SignIn/useSignIn.js +26 -0
- package/lib/util/cookieHelper.js +104 -0
- package/lib/util/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { setContext } from '@apollo/client/link/context';
|
|
2
|
-
import {
|
|
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
|
-
|
|
9
|
-
const token = storage.getItem('signin_token');
|
|
12
|
+
let token = null;
|
|
10
13
|
|
|
11
|
-
//
|
|
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
|
-
|
|
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']
|
package/lib/context/cart.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import 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)) {
|
package/lib/context/user.js
CHANGED
|
@@ -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
|
-
//
|
|
29
|
+
// Check for authentication tokens (localStorage or cookie)
|
|
29
30
|
const storage = new BrowserPersistence();
|
|
30
|
-
|
|
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
|
|
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
|
};
|
|
@@ -37,17 +37,20 @@ export const useProduct = props => {
|
|
|
37
37
|
] = useAppContext();
|
|
38
38
|
|
|
39
39
|
const { data: storeConfigData } = useQuery(getStoreConfigData, {
|
|
40
|
-
fetchPolicy: 'cache-
|
|
41
|
-
nextFetchPolicy: 'cache-first'
|
|
40
|
+
fetchPolicy: 'cache-first'
|
|
42
41
|
});
|
|
43
42
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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-
|
|
50
|
-
nextFetchPolicy: 'cache-
|
|
51
|
+
fetchPolicy: 'cache-first',
|
|
52
|
+
nextFetchPolicy: 'cache-only',
|
|
53
|
+
returnPartialData: true,
|
|
51
54
|
skip: !storeConfigData,
|
|
52
55
|
variables: {
|
|
53
56
|
urlKey
|
|
@@ -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