Shopify Embedded Apps and Safari

Safari now blocks all third-party cookies by default — how we got our apps working again

‘Allow access’ page screen-shot

Storage Access API to the rescue

Our environment

Shopify tutorial yields ‘Enable cookies’ endless redirect loop

Steps to fix apps for Safari

1. Do not rely on a cookie for shop origin

function getShopOriginFromUrl() {
var url = new URL(window.location.href);
return url.searchParams.get('shop');
}
const config = {
apiKey: process.env.REACT_APP_SHOPIFY_API_KEY,
shopOrigin: getShopOriginFromUrl(),
forceRedirect: true
};
class MyShopifyApp extends Component {
render() {
return (
<AppProvider i18n={translations}>
<Provider config={config}>
<App />
</Provider>
</AppProvider>
);
}
}
const root = document.getElementById('myShopifyApp');
ReactDOM.render(<MyShopifyApp />, root);

2. Check if cookies can be read

export default class App extends Component {
constructor(props) {
super(props);
this.state = {
hasCookieAccess: false
}
}
render() {
if (!this.state.hasCookieAccess) {
return (null);
}
// render app...
}
componentDidMount() {
const testCookie = Cookies.get('checkcookie');
if (typeof testCookie !== 'undefined') {
this.setState({ hasCookieAccess: true });
}
}
}

3. Redirect to a page in the top window

export default class App extends Component {
static contextType = Context;
// ... componentDidMount() {
this.redirectIfNoCookieAccess();
}
redirectIfNoCookieAccess() {
const testCookie = Cookies.get('checkcookie');
if (typeof testCookie !== 'undefined') {
this.setState({ hasCookieAccess: true });
return;
}
const app = this.context;
const redirect = Redirect.create(app);
let url = window.location.origin
+ this.getApplicationPath(window.location.pathname)
+ '/setcookie';
redirect.dispatch(Redirect.Action.REMOTE, url
+ window.location.search
+ '&r=' + encodeURIComponent(window.location.href));
}
getApplicationPath(pathname) {
let pos = pathname.substring(1).indexOf("/");
if (pos === -1)
return pathname;
return pathname.substring(0, pos + 1);
}
}

4. Set first-party cookie for session and redirect back to the app

5. Render user activation page

export default class App extends Component {
static contextType = Context;
constructor(props) {
super(props);
this.state = {
hasCookieAccess: false,
returnedFromRedirect: false
}
}
render() {
if (!this.state.hasCookieAccess
&& !this.state.returnedFromRedirect) {
return (null);
}
if (window.location === window.top.location) {
return (null);
}
if (!this.state.hasCookieAccess) {
return (
<UserActivate
hasCookieAccess={this.state.hasCookieAccess}
onChange={this.userActivateChange.bind(this)}
/>
);
}
// render app...
}
componentDidMount() {
var url = new URL(window.location.href);
if (url.searchParams.get('returnfromtop') !== '1') {
this.redirectIfNoCookieAccess();
return;
}
this.setState({ returnedFromRedirect: true });
}
// ... userActivateChange(field, value) {
this.setState({ [field]: value });
}
}

6. Request storage access

export default class UserActivate extends Component {
constructor(props) {
super(props);
this.activate = this.activate.bind(this);
}
render() {
return (
<Frame>
<Page>
<form className='allow-access-form'>
<Layout sectioned>
<Card title='Allow access' sectioned>
<p>This browser requires your permission to access this app. Please click the button to continue.</p>
<p><Button primary={true} onClick={this.activate}>Allow access</Button></p>
</Card>
</Layout>
</form>
</Page>
</Frame>
);
}
activate() {
if (document.hasStorageAccess()) {
this.props.onChange('hasCookieAccess', true);
}
var promise = document.requestStorageAccess();
promise.then(
function() {
this.props.onChange('hasCookieAccess', true);
},
function() {
console.log('ERROR: unable to get storage access.');
}
);
}
}

7. Render the app interface

Screen-shot of our app interface
Screen-shot of our app interface
Finally able to render our app

App extensions: painful user experience

Hopeful for improvement

Head of Engineering and Product at Firmwater. Staying home and washing my hands lots.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store