Skip to content

Commit

Permalink
Merge pull request #921 from input-output-hk/feat/lw-9716-analytics-o…
Browse files Browse the repository at this point in the history
…pt-in-out

Add legal section to settings LW-9716
  • Loading branch information
DominikGuzei committed Feb 15, 2024
2 parents 5370783 + 7e346e5 commit 1b72023
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 86 deletions.
46 changes: 5 additions & 41 deletions src/features/analytics/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,18 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import { usePostHog } from 'posthog-js/react';
import { Events, Properties } from './events';
import {
getAnalyticsConsent,
getEventMetadata,
setAnalyticsConsent,
} from './services';
import { getAnalyticsConsent, getEventMetadata } from './services';
import { useAnalyticsContext } from './provider';

/**
* Provides access to the analytics user consent stored
* in chrome.storage.local accessible in all parts of the
* extension UI. It exposes only the current consent and
* a method to toggle it.
*
* User consent is stored as boolean | undefined
*/
export const useAnalyticsConsent = (): [
boolean | undefined,
(consent: boolean) => Promise<void>,
] => {
// Store the consent in React state to trigger component updates
const [consent, setConsentState] = useState<boolean | undefined>();
// Fetch the stored user consent and assign to React state

useEffect(() => {
(async function () {
setConsentState(await getAnalyticsConsent());
})();
}, []);

return [
consent,
async (consent) => {
// Allow to set the consent state and store it too
await setAnalyticsConsent(consent);
setConsentState(consent);
},
];
};

export const useCaptureEvent = () => {
const posthog = usePostHog();
const view = useAnalyticsContext();
const [analytics] = useAnalyticsContext();

const captureEvent = useCallback(
async (event: Events, properties: Properties = {}) => {
const [hasConsent, metadata] = await Promise.all([
getAnalyticsConsent(),
getEventMetadata(view),
getEventMetadata(analytics.view),
]);

if (posthog && hasConsent) {
Expand All @@ -58,7 +22,7 @@ export const useCaptureEvent = () => {
});
}
},
[posthog, view]
[posthog, analytics.view]
);

return captureEvent;
Expand Down
115 changes: 82 additions & 33 deletions src/features/analytics/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,77 +2,126 @@ import { PostHogProvider } from 'posthog-js/react';
import { PostHogConfig } from 'posthog-js';
import React, {
ReactNode,
useEffect,
useMemo,
useState,
createContext,
useContext,
useState,
useEffect,
} from 'react';
import { getAnalyticsConsent, getUserId } from './services';
import { getOptions } from './posthog';
import { ExtensionViews } from './types';
import { POSTHOG_API_KEY } from './config';
import {
getAnalyticsConsent,
getUserId,
setAnalyticsConsent,
} from './services';

interface Props {
children: ReactNode;
view: ExtensionViews;
}

interface State {
/**
* Represents the user's consent to tracking analytics events
* as well as the current extension view for tracked events
*/
interface AnalyticsState {
consent?: boolean;
userId: string;
userId?: string;
view: ExtensionViews;
}

const ExtensionViewsContext = createContext<ExtensionViews | null>(null);

export const useAnalyticsContext = () => {
const analyticsContext = useContext(ExtensionViewsContext);
if (analyticsContext === null) throw new Error('context not defined');
return analyticsContext;
};

export const AnalyticsProvider = ({ children, view }: Props) => {
const [state, setState] = useState<State>();
/**
* Provides access to the AnalyticsState and handling
* the storage of userId and consent in chrome.storage.local
*/
const useAnalyticsState = (
view: ExtensionViews
): [AnalyticsState, (consent: boolean) => Promise<void>] => {
// Store the consent in React state to trigger component updates
const [consentState, setConsentState] = useState<AnalyticsState>({
view,
});

// Fetch the stored user consent and assign to React state
useEffect(() => {
const init = async () => {
(async function () {
const [consent, userId] = await Promise.all([
getAnalyticsConsent(),
getUserId(),
]);

setState({
setConsentState({
consent,
userId,
view,
});
};

init();
})();
}, []);

return [
consentState,
async (consent) => {
// Allow to set the consent state and store it too
await setAnalyticsConsent(consent);
setConsentState({
consent,
userId: consentState.userId,
view,
});
},
];
};

/**
* The analytics React context which is exposed by the hook below
*/
const AnalyticsContext = createContext<ReturnType<
typeof useAnalyticsState
> | null>(null);

/**
* The public hook that should be used by components to interact with the
* analytics state.
*/
export const useAnalyticsContext = () => {
const analyticsContext = useContext(AnalyticsContext);
if (analyticsContext === null) throw new Error('context not defined');
return analyticsContext;
};

/**
* The analytics provider that wraps the current extension
* view to set up the PostHog provider and the API to interact
* with the analytics state.
*/
export const AnalyticsProvider = ({
children,
view,
}: {
children: ReactNode;
view: ExtensionViews;
}) => {
const [analyticsState, setAnalyticsConsent] = useAnalyticsState(view);

const options = useMemo<Partial<PostHogConfig> | undefined>(() => {
const id = state?.userId;
const id = analyticsState?.userId;

if (id === undefined) {
return undefined;
}

return getOptions(id);
}, [state?.userId]);
}, [analyticsState?.userId]);

if (state?.consent === false || options === undefined) {
if (analyticsState?.consent === false || options === undefined) {
return (
<ExtensionViewsContext.Provider value={view}>
<AnalyticsContext.Provider value={[analyticsState, setAnalyticsConsent]}>
{children}
</ExtensionViewsContext.Provider>
</AnalyticsContext.Provider>
);
}

return (
<PostHogProvider apiKey={POSTHOG_API_KEY} options={options}>
<ExtensionViewsContext.Provider value={view}>
<AnalyticsContext.Provider value={[analyticsState, setAnalyticsConsent]}>
{children}
</ExtensionViewsContext.Provider>
</AnalyticsContext.Provider>
</PostHogProvider>
);
};
26 changes: 18 additions & 8 deletions src/features/analytics/ui/AnalyticsConsentModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,34 @@ export const AnalyticsConsentModal = ({ askForConsent, setConsent }) => {
isCentered
onClose={() => setConsent(false)}
blockScrollOnMount={false}
closeOnOverlayClick={false}
>
<ModalOverlay />
<ModalContent>
<ModalHeader fontSize="md">Legal & analytics</ModalHeader>
<ModalCloseButton />
<ModalHeader fontSize="md">Legal & Analytics</ModalHeader>
<ModalBody>
<Text
mb="1"
fontSize="md"
fontWeight="bold"
id="terms-of-service-agreement"
>
Give us a hand to improve your experience
Give us a hand to improve your Nami experience
</Text>
<Text fontSize="sm">
By sharing analytics data from your browser, you can help us
improve the quality and performance of Nami. For more information
on our privacy practices, please see our&nbsp;
We would like to collect anonymous information from your browser
extension to help us improve the quality and performance of Nami.
This may include data about how you use our service, your
preferences and information about your system. You can always
opt-out (see the&nbsp;
<Link
onClick={() => window.open('https://www.namiwallet.io/')}
textDecoration="underline"
>
FAQ
</Link>
&nbsp;for more details). For more information on our privacy
practices, see our&nbsp;
<Link
onClick={() => privacyPolRef.current.openModal()}
textDecoration="underline"
Expand All @@ -52,10 +62,10 @@ export const AnalyticsConsentModal = ({ askForConsent, setConsent }) => {
</ModalBody>
<ModalFooter>
<Button mr={3} variant="ghost" onClick={() => setConsent(false)}>
Decline
No thanks
</Button>
<Button colorScheme="teal" onClick={() => setConsent(true)}>
Accept
I agree
</Button>
</ModalFooter>
</ModalContent>
Expand Down
102 changes: 102 additions & 0 deletions src/features/settings/legal/LegalSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
Box,
Button,
Flex,
Link,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Spacer,
Switch,
Text,
Tooltip,
} from '@chakra-ui/react';
import React, { useRef } from 'react';
import { ChevronRightIcon, InfoOutlineIcon } from '@chakra-ui/icons';
import PrivacyPolicy from '../../../ui/app/components/privacyPolicy';
import TermsOfUse from '../../../ui/app/components/termsOfUse';
import { useAnalyticsContext } from '../../analytics/provider';

export const LegalSettings = () => {
const [analytics, setAnalyticsConsent] = useAnalyticsContext();
const termsRef = useRef<{ openModal: () => void }>();
const privacyPolicyRef = useRef<{ openModal: () => void }>();
return (
<>
<Box height="10" />
<Text fontSize="lg" fontWeight="bold">
Legal
</Text>
<Box height="6" />
<Flex minWidth="65%" padding="0 16px" alignItems="center" gap="2">
<Text fontSize="16" fontWeight="bold">
Analytics
<Popover autoFocus={false}>
<PopoverTrigger>
<InfoOutlineIcon
cursor="pointer"
color="#4A5568"
ml="10px"
width="14px"
height="14px"
display="inline-block"
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Text
color="grey"
fontWeight="500"
fontSize="14"
lineHeight="24px"
>
We collect anonymous information from your browser extension
to help us improve the quality and performance of Nami. This
may include data about how you use our service, your
preferences and information about your system. Read more&nbsp;
<Link
onClick={() => window.open('https://namiwallet.io')}
textDecoration="underline"
>
here
</Link>
.
</Text>
</PopoverBody>
</PopoverContent>
</Popover>
</Text>
<Spacer />
<Switch
isChecked={analytics.consent}
onChange={() => setAnalyticsConsent(!analytics.consent)}
/>
</Flex>
<Box height="3" />
<Button
justifyContent="space-between"
width="65%"
rightIcon={<ChevronRightIcon />}
variant="ghost"
onClick={() => termsRef.current?.openModal()}
>
Terms of Use
</Button>
<Box height="1" />
<Button
justifyContent="space-between"
width="65%"
rightIcon={<ChevronRightIcon />}
variant="ghost"
onClick={() => privacyPolicyRef.current?.openModal()}
>
Privacy Policy
</Button>
<PrivacyPolicy ref={privacyPolicyRef} />
<TermsOfUse ref={termsRef} />
</>
);
};
Loading

0 comments on commit 1b72023

Please sign in to comment.