Skip to content

Commit eab7cd2

Browse files
del15881del22123
andauthored
PWA-3569:IOS PWA-Webapp user login support (#4588)
Co-authored-by: Bharathidasan Elangovan <del22123@adobe.com>
1 parent 60fe7f2 commit eab7cd2

8 files changed

Lines changed: 454 additions & 13 deletions

File tree

packages/peregrine/lib/Apollo/links/authLink.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
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';
37

48
const storage = new BrowserPersistence();
59

610
export default function createAuthLink() {
711
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;
1013

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
1226
return {
1327
headers: {
1428
...headers,

packages/peregrine/lib/Apollo/links/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ export const customFetchToShrinkQuery = (uri, options) => {
2828

2929
const resource = options.method === 'GET' ? shrinkQuery(uri) : uri;
3030

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);
3239
};
3340

3441
const getLinks = apiBase => {

packages/peregrine/lib/context/user.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import actions from '../store/actions/user/actions';
55
import * as asyncActions from '../store/actions/user/asyncActions';
66
import bindActionCreators from '../util/bindActionCreators';
77
import BrowserPersistence from '../util/simplePersistence';
8+
import { getTokenFromCookie, shouldPreferCookies } from '../util/cookieHelper';
89

910
const UserContext = createContext();
1011

@@ -25,20 +26,35 @@ const UserContextProvider = props => {
2526
]);
2627

2728
useEffect(() => {
28-
// check if the user's token is not expired
29+
// Check for authentication tokens (localStorage or cookie)
2930
const storage = new BrowserPersistence();
30-
const item = storage.getRawItem('signin_token');
31+
let hasValidToken = false;
3132

33+
// First, check localStorage token (existing behavior)
34+
const item = storage.getRawItem('signin_token');
3235
if (item) {
3336
const { ttl, timeStored } = JSON.parse(item);
3437
const now = Date.now();
3538

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
3740
if (ttl && now - timeStored > ttl * 1000) {
3841
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;
3955
}
4056
}
41-
}, [asyncActions]);
57+
}, [asyncActions, actions, userState]);
4258

4359
return (
4460
<UserContext.Provider value={contextValue}>

packages/peregrine/lib/store/actions/user/asyncActions.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ export const clearToken = () =>
8686
// Clear token from local storage
8787
storage.removeItem('signin_token');
8888

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+
89104
// Remove from store
90105
dispatch(actions.clearToken());
91106
};

packages/peregrine/lib/talons/SignIn/useSignIn.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,41 @@ export const useSignIn = props => {
122122
});
123123

124124
const token = signInResponse.data.generateCustomerToken.token;
125+
await (customerAccessTokenLifetime
126+
? setToken(token, customerAccessTokenLifetime)
127+
: setToken(token));
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+
}
125154

126155
// Clear all cart/customer data from cache and redux.
127156
await apolloClient.clearCacheData(apolloClient, 'cart');
128157
await apolloClient.clearCacheData(apolloClient, 'customer');
129158
await removeCart();
130159

131-
await (customerAccessTokenLifetime
132-
? setToken(token, customerAccessTokenLifetime)
133-
: setToken(token));
134-
135160
// Create and get the customer's cart id.
136161
await createCart({
137162
fetchCartId

0 commit comments

Comments
 (0)