Google Consent Mode Implementation using Gatsby - javascript

I am following this tutorial about implementing google consent mode to add cookies to my website !
By using Gatsby.js I am not sure how to add these codes :
<!-- The initial config of Consent Mode -->
<script type="text/javascript">
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('consent', 'default', {
ad_storage: 'denied',
analytics_storage: 'denied',
wait_for_update: 1500,
});
gtag('set', 'ads_data_redaction', true);
</script>
​
<!-- Cookie Information Pop-up Script is required for the SDK -->
<script id="CookieConsent" src="https://policy.app.cookieinformation.com/uc.js" data-culture="EN" type="text/javascript"></script>
​
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=TRACKING-ID"></script>
<script type="text/javascript">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
​
gtag('config', 'TRACKING-ID');
</script>
​
</head>
<body>
Do you have any idea how to implement this code in Gatsby , is there any library or something that will help to implement these scripts !
Thanks

This component is used as the initial screen that applies when the page loads.
import React, { useState, useEffect } from 'react';
import { useLocation } from '#reach/router';
import { initializeAndTrack } from 'gatsby-plugin-gdpr-cookies';
import Cookies from 'js-cookie';
import CookieSettings from './Settings';
const CookieBanner = () => {
const [showBanner, setShowBanner] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const location = useLocation();
// showSettings -> use this state property to open a configuration
// window which may open up more information on the cookie(s) being applied
useEffect(() => {
setShowBanner(Cookies.get('gatsby-gdpr-responded') !== 'true');
}, [])
useEffect(() => {
initTracking();
}, [Cookies.get('gatsby-gdpr-responded')])
const initTracking = () => {
initializeAndTrack(location)
}
const handleAccept = () => {
Cookies.set('gatsby-gdpr-google-analytics', true, { expires: 365 })
handleCloseAll();
}
const handleDecline = () => {
Cookies.remove('gatsby-gdpr-google-analytics');
handleCloseAll();
}
const handleCloseAll = () => {
setShowSettings(false);
setShowBanner(false);
Cookies.set('gatsby-gdpr-responded', true, { expires: 365 });
}
return (
// add your component logic here
// Take not of the different functions that are available above, like handleAccept / handleDecline / handleCloseAll
// handleCloseAll -> if a user declines / closes the banner
// handleAccept -> a button to accept by default
// handleDecline -> a button to decline the cookies
)
}
export default CookieBanner
The next component is more of a Configuration screen, which provides more information on the cookies being applied, if you take note on the import of Toggle, we use a toggle to allow users to specifically toggle on or off their cookies at any point, you of course if you have many GDPR compliances, may want to either create separate functions that handle the removal of cookies or a reusable function that is passed the name of the cookie to be removed / applied.
import React, { useState } from 'react';
import Cookies from 'js-cookie';
import Button from '#components/Button';
import Toggle from '#components/Inputs/Toggle';
const CookieSettings = ({
handleAccept,
handleDecline,
initTracking,
handleCloseAll
}) => {
const [trackAnalytics, setTrackAnalytics] = useState(Cookies.get('gatsby-gdpr-google-analytics') === 'true')
const handleToggle = () => {
Cookies.set('gatsby-gdpr-responded', true, { expires: 365 });
setTrackAnalytics((prevState) => {
if (prevState) {
Cookies.remove('gatsby-gdpr-google-analytics');
} else {
Cookies.set('gatsby-gdpr-google-analytics', true, { expires: 365 })
}
return !prevState
})
initTracking();
}
return (
// your JSX code here
)
}
export default CookieSettings;
EDIT
// A some what reusable function that you can pass a cookie name too and switch over the name provided and set the required cookie.
const handleToggle = (cookieName) => {
Cookies.set('gatsby-gdpr-responded', true, { expires: 365 });
switch (cookieName) {
case 'gatsby-gdpr-google-analytics':
return setTrackAnalytics((prevState) => {
if (prevState) {
Cookies.remove(cookieName);
} else {
Cookies.set(cookieName, true, {
expires: 365
});
}
return !prevState
})
case 'gatsby-gdpr-google-tagmanager':
return setTagAnalytics((prevState) => {
if (prevState) {
Cookies.remove(cookieName);
} else {
Cookies.set(cookieName, true, {
expires: 365
});
}
return !prevState
})
case 'gatsby-gdpr-facebook-pixel':
return setFacebookAnalytics((prevState) => {
if (prevState) {
Cookies.remove(cookieName);
} else {
Cookies.set(cookieName, true, {
expires: 365
});
}
return !prevState
})
default:
break;
}
initTracking()
}
// A JSX toggle within your cookie setting
<Toggle active={trackAnalytics} toggleActive={() => handleToggle('gatsby-gdpr-google-analytics')} />
// The toggle component itself
import React from 'react';
import cx from 'classnames'
import PropTypes from 'prop-types'
import './styles.scss';
export default function Toggle({
active = false,
toggleActive,
}) {
return (
<div onClick={typeof toggleActive === 'function' && toggleActive} className={cx('toggle relative cursor-pointer', { active })} />
)
}
Toggle.propTypes = {
active: PropTypes.bool,
toggleActive: PropTypes.func.isRequired
}
Toggle.defaultProps = {
active: false,
}

Use this plugin from the Gatsby Plugin Hub
gatsby-plugin-gdpr-cookies
It will provide you what you are looking for and also you can list in the options for the plugin which cookies you are looking to track + a cookieName you wish to provide which you can then work with from a component level when creating a cookie toolbar such as:
{
resolve: `gatsby-plugin-gdpr-cookies`,
options: {
googleAnalytics: {
trackingId: process.env.UA_TAG, // your UA tag goes here
cookieName: `gatsby-gdpr-google-analytics`,
anonymize: true,
allowAdFeatures: false
},
environments: [`production`, `development`]
},
},
It elimites the usage of having to inject a script into the head of the website with React-Helmet as the plugin will handle the script injection for you.

Related

React load script on specfic div

I need to load a script on specific component in my React app.
Below is my script and i need this to load on bottom-most div in my component
<div id="rexxxx"></div>
<script>
new carouselInlineWidget("xx", {
/*Your REVIEWS.io account ID:*/
store: "xxxxxxxxxxxx",
sku: "",
lang: "en",
carousel_type: "xxxxx",
styles_carousel: "CarouselWidget--xxxxxx",
/*Widget settings:*/
options: {
general: {
/*What reviews should the widget display? Available options: company, product, third_party. You can choose one type or multiple separated by comma.*/
enable_auto_scroll: 10000,
},
header: {
},
reviews: {
},
popups: {},
},
styles: {
},
});
</script>
I have my React component
import React from 'react'
import edenredLogo from '../../images/edenred-logo.webp'
import { useHistory } from 'react-router-dom'
import { useSelector } from 'react-redux'
import './landing.less'
const Landing = () => {
const history = useHistory()
return (
<>
<div className="script-here"/>
</>
)
}
export default Landing
You can use the custom hook:
import { useEffect } from 'react';
const useScript = (url, position, async = true) => {
useEffect(() => {
const placement = document.querySelector(position);
const script = document.createElement('script');
script.src = url;
script.async = typeof async === 'undefined' ? true : async;
placement.appendChild(script);
return () => {
placement.removeChild(script);
};
}, [url]);
};
export default useScript;
Usage:
useScript(url, ".script-here");
Or just use dangerouslySetInnerHTML
<div className="script-here" dangerouslySetInnerHTML={{__html: your_script}} />

React Router v6 doesn't support usePrompt and useBlock anymore [duplicate]

I am basically trying to intercept route changes. Maybe something equivalent of vue's beforeEach in React Router v6 could be useful as React Router v.6 does not include usePrompt.
BEFORE each route change I want to do some logic - the logic might need to interrupt or even change the end route based on the result.
I have searched around but I really can't find something that solves this specific problem.
Thanks in advance.
Currently they have removed the usePrompt from the react-router v6.
I found a solution from ui.dev and added TypeScript support, and am now using that until the react-router will bring back the usePrompt/useBlocker hooks
import { History, Transition } from 'history';
import { useCallback, useContext, useEffect } from "react";
import { Navigator } from 'react-router';
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";
type ExtendNavigator = Navigator & Pick<History, "block">;
export function useBlocker(blocker: (tx: Transition) => void, when = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) return;
const unblock = (navigator as ExtendNavigator).block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
export default function usePrompt(message: string, when = true) {
const blocker = useCallback((tx: Transition) => {
if (window.confirm(message)) tx.retry();
}, [message]);
useBlocker(blocker, when);
}
This can then be used in any view/component where you would like a "A you sure you want to leave?"-message displayed when the condition is true.
usePrompt("Do you want to leave?", isFormDirty());
Yes usePrompt and useBlock has been removed, but you can achieve same thing using history.block, here is the working example for blocking navigation using history.block with custom modal in React Router Dom V5
import { useHistory } from "react-router-dom";
import { UnregisterCallback } from "history";
...
type Prop = {
verify?: {
blockRoute?: (nextRoute: string) => boolean;
}
};
...
// in the component where you want to show confirmation modal on any nav change
const history = useHistory();
const unblock = useRef<UnregisterCallback>();
const onConfirmExit = () => {
/**
* if user confirms to exit, we can allow the navigation
*/
// Unblock the navigation.
unblock?.current?.();
// Proceed with the blocked navigation
goBack();
};
useEffect(() => {
/**
* Block navigation and register a callback that
* fires when a navigation attempt is blocked.
*/
unblock.current = history.block(({ pathname: to }) => {
/**
* Simply allow the transition to pass immediately,
* if user does not want to verify the navigate away action,
* or if user is allowed to navigate to next route without blocking.
*/
if (!verify || !verify.blockRoute?.(to)) return undefined;
/**
* Navigation was blocked! Let's show a confirmation dialog
* so the user can decide if they actually want to navigate
* away and discard changes they've made in the current page.
*/
showConfirmationModal();
// prevent navigation
return false;
});
// just in case theres an unmount we can unblock if it exists
return unblock.current;
}, [history]);
Here is a JS example of the react-route-dom v6 usePrompt if you're not using TS.
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
export function useBlocker( blocker, when = true ) {
const { navigator } = useContext( NavigationContext );
useEffect( () => {
if ( ! when ) return;
const unblock = navigator.block( ( tx ) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};
blocker( autoUnblockingTx );
} );
return unblock;
}, [ navigator, blocker, when ] );
}
export function usePrompt( message, when = true ) {
const blocker = useCallback(
( tx ) => {
// eslint-disable-next-line no-alert
if ( window.confirm( message ) ) tx.retry();
},
[ message ]
);
useBlocker( blocker, when );
}
Then the implementation would be...
const MyComponent = () => {
const formIsDirty = true; // Condition to trigger the prompt.
usePrompt( 'Leave screen?', formIsDirty );
return (
<div>Hello world</div>
);
};
Here's the article with the example

vue3 - can't render facebook comments

I am trying to add a Facebook comments plugin (check it here) to my vue app, the problem is that div is created in DOM but it sometimes shows, sometimes not(width: 0, height 0)
Note: I am calling XFBML.parse function, my host is added to fb app
This is my current code:
<template>
<div
ref="commentContainer"
class="fb-comments"
:data-href="onUrl()"
:data-width="cwidth"
:data-numposts="numposts"
></div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, watch } from "vue";
import router from "../../router";
export default defineComponent({
props: {
cwidth: {
type: String,
default: "100%",
},
numposts: {
type: String,
default: "2",
},
},
setup({ cwidth, numposts }) {
const commentContainer = ref(null);
const init = () => {
if (
window.FB &&
!commentContainer.value.hasAttribute("fb-xfbml-state")
) {
setTimeout(() => {
window.FB.XFBML.parse(commentContainer.value.parentElement);
}, 2000);
}
};
onMounted(() => {
setTimeout(() => {
init();
}, 1500);
});
const onUrl = () => {
return document.location.origin + document.location.pathname;
};
watch(
() => router.currentRoute.value,
() => {
init();
}
);
return { cwidth, numposts, commentContainer, onUrl };
},
});
</script>
Instead of doing setTimeout try using nextTick and not passing any params to the parse function.
E.g. in the mounted function
this.$nextTick(() => {
window.FB.XFBML.parse()
})
Are you waiting 1.5s before running init() for a reason?
The above works using Vue2, for Vue3 example see below:
import { createApp, nextTick } from 'vue'
const app = createApp({
setup() {
const init = async () => {
await nextTick()
window.FB.XFBML.parse()
}
}
})
https://v3.vuejs.org/api/global-api.html#nexttick
Also, make sure you have added the SDK script and provided fb-root div to your index.html. It would not work on mine unless I added these just before the closing </body> tag.
I also had to add the the nextTick code to the route watcher to force the window to parse FB again when a new page is navigated to. I'm unsure of the Vue 3 version but I'm sure you can figure it out from this example:
watch: {
$route (to, from) {
if (to.fullPath !== from.fullPath) {
this.$nextTick(() => {
window.FB.XFBML.parse()
})
}
}
}

Implementing Cloudinary Product Gallery in Next.js

I am trying to use the Cloudinary Product Gallery in my eCommerce - Next.js project, but am having a difficult time putting it together.
Here is Cloudinary Product Gallery: https://cloudinary.com/documentation/product_gallery
The error I am getting: Cannot read property 'galleryWidget' of undefined . Let me know what I am doing wrong.
file - _document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
<Html>
<Head />
<body>
<script
src="https://product-gallery.cloudinary.com/all.js"
type="text/javascript"
></script>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
file - SingleProduct.jsx
import React, { useRef } from 'react';
const SingleProduct = ({ product }) => {
const { slug } = product;
const cloudnaryGalleryRef = useRef(null);
if (!cloudnaryGalleryRef.current) {
cloudnaryGalleryRef.current = window.cloudinary
.galleryWidget({
container: '#my-gallery',
cloudName: 'cloudName',
carouselStyle: 'thumbnails',
thumbnailProps: {
width: 75,
height: 75,
spacing: 4,
navigationColor: 'green',
},
mediaAssets: [{ tag: slug }],
})
.render();
}
return <div id="my-gallery"></div>;
};
export default SingleProduct;
Assuming there's no problem with your script, you need to check if window available before executing the function..
e.g.
if (typeof window !== 'undefined') {
..... window.cloudinary.galleryWidget(.....)
}
You will notice this will occur very frequently in nextjs apps, and you need to include these necessary checks since next.js in most cases are server side generated, and until pages are loaded in browser, window is not defined.
Here is an working solution
import React, { useRef } from 'react';
const SingleProduct = ({ product }) => {
const { slug } = product;
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const scriptTag = document.createElement('script');
scriptTag.src = 'https://product-gallery.cloudinary.com/all.js';
scriptTag.addEventListener('load', () => setLoaded(true));
document.body.appendChild(scriptTag);
}, []);
const cloudnaryGalleryRef = useRef(null);
useEffect(() => {
if (!loaded) return;
const myGallery = window.cloudinary.galleryWidget({
container: '#my-gallery',
cloudName: 'dhvi46rif',
carouselStyle: 'thumbnails', // default value: included for clarity
thumbnailProps: {
width: 75,
height: 75,
spacing: 4,
navigationColor: 'green',
},
mediaAssets: [{ tag: slug }],
});
if (!cloudnaryGalleryRef.current && typeof window !== 'undefined') {
cloudnaryGalleryRef.current = myGallery.render();
}
return () => {
cloudnaryGalleryRef.current = myGallery.destroy(); // Important To avoid memory leaks and performance issues, make sure to use the destroy method before removing the Product Gallery widget container element from your DOM.
};
}, [loaded, slug]);
return <div id="my-gallery" />;
};
export default SingleProduct;

How to use a custom component with react-router route transitions?

The article Confirming Navigation explains how to use a browser confirmation box in your transition hook. Fine. But I want to use my own Dialog box. If I were to use the methods from the history module I think this is possible. Is it possible to do this with the setRouteLeaveHook in react-router?
The core problem is that setRouteLeaveHook expects the hook function to return its result synchronously. This means you don't have the time to display a custom dialog component, wait for the user to click an option, and then return the result. So we need a way to specify an asynchronous hook. Here's a utility function I wrote:
// Asynchronous version of `setRouteLeaveHook`.
// Instead of synchronously returning a result, the hook is expected to
// return a promise.
function setAsyncRouteLeaveHook(router, route, hook) {
let withinHook = false
let finalResult = undefined
let finalResultSet = false
router.setRouteLeaveHook(route, nextLocation => {
withinHook = true
if (!finalResultSet) {
hook(nextLocation).then(result => {
finalResult = result
finalResultSet = true
if (!withinHook && nextLocation) {
// Re-schedule the navigation
router.push(nextLocation)
}
})
}
let result = finalResultSet ? finalResult : false
withinHook = false
finalResult = undefined
finalResultSet = false
return result
})
}
Here is an example of how to use it, using vex to show a dialog box:
componentWillMount() {
setAsyncRouteLeaveHook(this.context.router, this.props.route, this.routerWillLeave)
}
​
routerWillLeave(nextLocation) {
return new Promise((resolve, reject) => {
if (!this.state.textValue) {
// No unsaved changes -- leave
resolve(true)
} else {
// Unsaved changes -- ask for confirmation
vex.dialog.confirm({
message: 'There are unsaved changes. Leave anyway?' + nextLocation,
callback: result => resolve(result)
})
}
})
}
I made it work by setting a boolean on state whether you have confirmed to navigate away (using react-router 2.8.x). As it says in the link you posted:
https://github.com/ReactTraining/react-router/blob/master/docs/guides/ConfirmingNavigation.md
return false to prevent a transition w/o prompting the user
However, they forget to mention that the hook should be unregistered as well, see here and here.
We can use this to implement our own solution as follows:
class YourComponent extends Component {
constructor() {
super();
const {route} = this.props;
const {router} = this.context;
this.onCancel = this.onCancel.bind(this);
this.onConfirm = this.onConfirm.bind(this);
this.unregisterLeaveHook = router.setRouteLeaveHook(
route,
this.routerWillLeave.bind(this)
);
}
componentWillUnmount() {
this.unregisterLeaveHook();
}
routerWillLeave() {
const {hasConfirmed} = this.state;
if (!hasConfirmed) {
this.setState({showConfirmModal: true});
// Cancel route change
return false;
}
// User has confirmed. Navigate away
return true;
}
onCancel() {
this.setState({showConfirmModal: false});
}
onConfirm() {
this.setState({hasConfirmed: true, showConfirmModal: true}, function () {
this.context.router.goBack();
}.bind(this));
}
render() {
const {showConfirmModal} = this.state;
return (
<ConfirmModal
isOpen={showConfirmModal}
onCancel={this.onCancel}
onConfirm={this.onConfirm} />
);
}
}
YourComponent.contextTypes = {
router: routerShape
};
Posting my solution for intercept back button or even a route change. This works with React-router 2.8 or higher. Or even with withRouter
import React, {PropTypes as T} from 'react';
...
componentWillMount() {
this.context.router.setRouteLeaveHook(this.props.route, this.routerWillLeaveCallback.bind(this));
}
routerWillLeaveCallback(nextLocation) {
let showModal = this.state.unsavedChanges;
if (showModal) {
this.setState({
openUnsavedDialog: true,
unsavedResolveCallback: Promise.resolve
});
return false;
}
return true;
}
}
YourComponent.contextTypes = {
router: T.object.isRequired
};
Above is great except when user goes back in history. Something like the following should fix the problem:
if (!withinHook && nextLocation) {
if (nextLocation.action=='POP') {
router.goBack()
} else {
router.push(nextLocation)
}
}
Here's my solution to the same. I made a custom dialog component that you can use to wrap any component in your app. You can wrap your header and this way have it present on all pages. It assumes you're using Redux Form, but you can simply replace areThereUnsavedChanges with some other form change checking code. It also uses React Bootstrap modal, which again you can replace with your own custom dialog.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { withRouter, browserHistory } from 'react-router'
import { translate } from 'react-i18next'
import { Button, Modal, Row, Col } from 'react-bootstrap'
// have to use this global var, because setState does things at unpredictable times and dialog gets presented twice
let navConfirmed = false
#withRouter
#connect(
state => ({ form: state.form })
)
export default class UnsavedFormModal extends Component {
constructor(props) {
super(props)
this.areThereUnsavedChanges = this.areThereUnsavedChanges.bind(this)
this.state = ({ unsavedFormDialog: false })
}
areThereUnsavedChanges() {
return this.props.form && Object.values(this.props.form).length > 0 &&
Object.values(this.props.form)
.findIndex(frm => (Object.values(frm)
.findIndex(field => field && field.initial && field.initial !== field.value) !== -1)) !== -1
}
render() {
const moveForward = () => {
this.setState({ unsavedFormDialog: false })
navConfirmed = true
browserHistory.push(this.state.nextLocation.pathname)
}
const onHide = () => this.setState({ unsavedFormDialog: false })
if (this.areThereUnsavedChanges() && this.props.router && this.props.routes && this.props.routes.length > 0) {
this.props.router.setRouteLeaveHook(this.props.routes[this.props.routes.length - 1], (nextLocation) => {
if (navConfirmed || !this.areThereUnsavedChanges()) {
navConfirmed = false
return true
} else {
this.setState({ unsavedFormDialog: true, nextLocation: nextLocation })
return false
}
})
}
return (
<div>
{this.props.children}
<Modal show={this.state.unsavedFormDialog} onHide={onHide} bsSize="sm" aria-labelledby="contained-modal-title-md">
<Modal.Header>
<Modal.Title id="contained-modal-title-md">WARNING: unsaved changes</Modal.Title>
</Modal.Header>
<Modal.Body>
Are you sure you want to leave the page without saving changes to the form?
<Row>
<Col xs={6}><Button block onClick={onHide}>Cancel</Button></Col>
<Col xs={6}><Button block onClick={moveForward}>OK</Button></Col>
</Row>
</Modal.Body>
</Modal>
</div>
)
}
}

Categories