From 05c4e342b33e4c355a81ea084f5d46e3bee70bfe Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Wed, 27 Nov 2024 10:12:20 -0500 Subject: [PATCH 1/7] Sets up the Data and Privacy screen to start. Still needs styling etc. --- src/renderer/src/colors.ts | 2 + .../src/components/OnboardingTopMenu.tsx | 45 +++++++ src/renderer/src/images/LockedWithKey.svg | 3 + src/renderer/src/routeTree.gen.ts | 113 ++++++++++-------- .../src/routes/Onboarding.DataPrivacy.tsx | 14 --- .../src/routes/Onboarding/DataPrivacy.tsx | 73 +++++++++++ .../src/routes/Onboarding/NextStep.tsx | 10 ++ .../{Onboarding.tsx => Onboarding/index.tsx} | 22 ++-- 8 files changed, 207 insertions(+), 75 deletions(-) create mode 100644 src/renderer/src/components/OnboardingTopMenu.tsx create mode 100644 src/renderer/src/images/LockedWithKey.svg delete mode 100644 src/renderer/src/routes/Onboarding.DataPrivacy.tsx create mode 100644 src/renderer/src/routes/Onboarding/DataPrivacy.tsx create mode 100644 src/renderer/src/routes/Onboarding/NextStep.tsx rename src/renderer/src/routes/{Onboarding.tsx => Onboarding/index.tsx} (88%) diff --git a/src/renderer/src/colors.ts b/src/renderer/src/colors.ts index 9476147..6dbad44 100644 --- a/src/renderer/src/colors.ts +++ b/src/renderer/src/colors.ts @@ -1,7 +1,9 @@ export const COMAPEO_BLUE = '#0066FF' export const DARK_COMAPEO_BLUE = '#050F77' export const LIGHT_COMAPEO_BLUE = '#CCE0FF' + export const CORNFLOWER_BLUE = '#80A0FF' +export const BLUE_GREY = '#CCCCD6' export const ORANGE = '#FF9933' export const DARK_ORANGE = '#E86826' diff --git a/src/renderer/src/components/OnboardingTopMenu.tsx b/src/renderer/src/components/OnboardingTopMenu.tsx new file mode 100644 index 0000000..c465e53 --- /dev/null +++ b/src/renderer/src/components/OnboardingTopMenu.tsx @@ -0,0 +1,45 @@ +import { styled } from '@mui/material/styles' +import { useNavigate } from '@tanstack/react-router' + +const MenuContainer = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '90%', + marginBottom: '32px', + color: '#fff', +}) + +const Steps = styled('div')({ + display: 'flex', + alignItems: 'center', + gap: '8px', +}) + +const Step = styled('div')(({ active }: { active: boolean }) => ({ + backgroundColor: active ? '#fff' : 'transparent', + color: active ? '#000' : '#fff', + padding: '4px 12px', + borderRadius: '16px', +})) + +interface OnboardingTopMenuProps { + currentStep: number +} + +export function OnboardingTopMenu({ currentStep }: OnboardingTopMenuProps) { + const navigate = useNavigate() + + return ( + + + + {['Step 1', 'Step 2', 'Step 3'].map((step, index) => ( + + {step} + + ))} + + + ) +} diff --git a/src/renderer/src/images/LockedWithKey.svg b/src/renderer/src/images/LockedWithKey.svg new file mode 100644 index 0000000..2daad0e --- /dev/null +++ b/src/renderer/src/images/LockedWithKey.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/renderer/src/routeTree.gen.ts b/src/renderer/src/routeTree.gen.ts index bc13be6..1ee939b 100644 --- a/src/renderer/src/routeTree.gen.ts +++ b/src/renderer/src/routeTree.gen.ts @@ -13,8 +13,9 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as MapTabsMapImport } from './routes/(MapTabs)/_Map' import { Route as MapTabsMapTab1Import } from './routes/(MapTabs)/_Map.tab1' import { Route as MapTabsMapTab2Import } from './routes/(MapTabs)/_Map.tab2' -import { Route as OnboardingImport } from './routes/Onboarding' -import { Route as OnboardingDataPrivacyImport } from './routes/Onboarding.DataPrivacy' +import { Route as OnboardingDataPrivacyImport } from './routes/Onboarding/DataPrivacy' +import { Route as OnboardingNextStepImport } from './routes/Onboarding/NextStep' +import { Route as OnboardingIndexImport } from './routes/Onboarding/index' import { Route as WelcomeImport } from './routes/Welcome' // Import Routes @@ -38,22 +39,28 @@ const WelcomeRoute = WelcomeImport.update({ getParentRoute: () => rootRoute, } as any) -const OnboardingRoute = OnboardingImport.update({ - id: '/Onboarding', - path: '/Onboarding', - getParentRoute: () => rootRoute, -} as any) - const IndexRoute = IndexImport.update({ id: '/', path: '/', getParentRoute: () => rootRoute, } as any) +const OnboardingIndexRoute = OnboardingIndexImport.update({ + id: '/Onboarding/', + path: '/Onboarding/', + getParentRoute: () => rootRoute, +} as any) + +const OnboardingNextStepRoute = OnboardingNextStepImport.update({ + id: '/Onboarding/NextStep', + path: '/Onboarding/NextStep', + getParentRoute: () => rootRoute, +} as any) + const OnboardingDataPrivacyRoute = OnboardingDataPrivacyImport.update({ - id: '/DataPrivacy', - path: '/DataPrivacy', - getParentRoute: () => OnboardingRoute, + id: '/Onboarding/DataPrivacy', + path: '/Onboarding/DataPrivacy', + getParentRoute: () => rootRoute, } as any) const MapTabsMapRoute = MapTabsMapImport.update({ @@ -84,13 +91,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } - '/Onboarding': { - id: '/Onboarding' - path: '/Onboarding' - fullPath: '/Onboarding' - preLoaderRoute: typeof OnboardingImport - parentRoute: typeof rootRoute - } '/Welcome': { id: '/Welcome' path: '/Welcome' @@ -114,10 +114,24 @@ declare module '@tanstack/react-router' { } '/Onboarding/DataPrivacy': { id: '/Onboarding/DataPrivacy' - path: '/DataPrivacy' + path: '/Onboarding/DataPrivacy' fullPath: '/Onboarding/DataPrivacy' preLoaderRoute: typeof OnboardingDataPrivacyImport - parentRoute: typeof OnboardingImport + parentRoute: typeof rootRoute + } + '/Onboarding/NextStep': { + id: '/Onboarding/NextStep' + path: '/Onboarding/NextStep' + fullPath: '/Onboarding/NextStep' + preLoaderRoute: typeof OnboardingNextStepImport + parentRoute: typeof rootRoute + } + '/Onboarding/': { + id: '/Onboarding/' + path: '/Onboarding' + fullPath: '/Onboarding' + preLoaderRoute: typeof OnboardingIndexImport + parentRoute: typeof rootRoute } '/(MapTabs)/_Map/tab1': { id: '/(MapTabs)/_Map/tab1' @@ -138,18 +152,6 @@ declare module '@tanstack/react-router' { // Create and export the route tree -interface OnboardingRouteChildren { - OnboardingDataPrivacyRoute: typeof OnboardingDataPrivacyRoute -} - -const OnboardingRouteChildren: OnboardingRouteChildren = { - OnboardingDataPrivacyRoute: OnboardingDataPrivacyRoute, -} - -const OnboardingRouteWithChildren = OnboardingRoute._addFileChildren( - OnboardingRouteChildren, -) - interface MapTabsMapRouteChildren { MapTabsMapTab1Route: typeof MapTabsMapTab1Route MapTabsMapTab2Route: typeof MapTabsMapTab2Route @@ -177,18 +179,20 @@ const MapTabsRouteWithChildren = export interface FileRoutesByFullPath { '/': typeof MapTabsMapRouteWithChildren - '/Onboarding': typeof OnboardingRouteWithChildren '/Welcome': typeof WelcomeRoute '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute + '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding': typeof OnboardingIndexRoute '/tab1': typeof MapTabsMapTab1Route '/tab2': typeof MapTabsMapTab2Route } export interface FileRoutesByTo { '/': typeof MapTabsMapRouteWithChildren - '/Onboarding': typeof OnboardingRouteWithChildren '/Welcome': typeof WelcomeRoute '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute + '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding': typeof OnboardingIndexRoute '/tab1': typeof MapTabsMapTab1Route '/tab2': typeof MapTabsMapTab2Route } @@ -196,11 +200,12 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute - '/Onboarding': typeof OnboardingRouteWithChildren '/Welcome': typeof WelcomeRoute '/(MapTabs)': typeof MapTabsRouteWithChildren '/(MapTabs)/_Map': typeof MapTabsMapRouteWithChildren '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute + '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding/': typeof OnboardingIndexRoute '/(MapTabs)/_Map/tab1': typeof MapTabsMapTab1Route '/(MapTabs)/_Map/tab2': typeof MapTabsMapTab2Route } @@ -209,27 +214,30 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' - | '/Onboarding' | '/Welcome' | '/Onboarding/DataPrivacy' + | '/Onboarding/NextStep' + | '/Onboarding' | '/tab1' | '/tab2' fileRoutesByTo: FileRoutesByTo to: | '/' - | '/Onboarding' | '/Welcome' | '/Onboarding/DataPrivacy' + | '/Onboarding/NextStep' + | '/Onboarding' | '/tab1' | '/tab2' id: | '__root__' | '/' - | '/Onboarding' | '/Welcome' | '/(MapTabs)' | '/(MapTabs)/_Map' | '/Onboarding/DataPrivacy' + | '/Onboarding/NextStep' + | '/Onboarding/' | '/(MapTabs)/_Map/tab1' | '/(MapTabs)/_Map/tab2' fileRoutesById: FileRoutesById @@ -237,16 +245,20 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute - OnboardingRoute: typeof OnboardingRouteWithChildren WelcomeRoute: typeof WelcomeRoute MapTabsRoute: typeof MapTabsRouteWithChildren + OnboardingDataPrivacyRoute: typeof OnboardingDataPrivacyRoute + OnboardingNextStepRoute: typeof OnboardingNextStepRoute + OnboardingIndexRoute: typeof OnboardingIndexRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, - OnboardingRoute: OnboardingRouteWithChildren, WelcomeRoute: WelcomeRoute, MapTabsRoute: MapTabsRouteWithChildren, + OnboardingDataPrivacyRoute: OnboardingDataPrivacyRoute, + OnboardingNextStepRoute: OnboardingNextStepRoute, + OnboardingIndexRoute: OnboardingIndexRoute, } export const routeTree = rootRoute @@ -260,20 +272,16 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", - "/Onboarding", "/Welcome", - "/(MapTabs)" + "/(MapTabs)", + "/Onboarding/DataPrivacy", + "/Onboarding/NextStep", + "/Onboarding/" ] }, "/": { "filePath": "index.tsx" }, - "/Onboarding": { - "filePath": "Onboarding.tsx", - "children": [ - "/Onboarding/DataPrivacy" - ] - }, "/Welcome": { "filePath": "Welcome.tsx" }, @@ -292,8 +300,13 @@ export const routeTree = rootRoute ] }, "/Onboarding/DataPrivacy": { - "filePath": "Onboarding.DataPrivacy.tsx", - "parent": "/Onboarding" + "filePath": "Onboarding/DataPrivacy.tsx" + }, + "/Onboarding/NextStep": { + "filePath": "Onboarding/NextStep.tsx" + }, + "/Onboarding/": { + "filePath": "Onboarding/index.tsx" }, "/(MapTabs)/_Map/tab1": { "filePath": "(MapTabs)/_Map.tab1.tsx", diff --git a/src/renderer/src/routes/Onboarding.DataPrivacy.tsx b/src/renderer/src/routes/Onboarding.DataPrivacy.tsx deleted file mode 100644 index 41dc365..0000000 --- a/src/renderer/src/routes/Onboarding.DataPrivacy.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react' -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/Onboarding/DataPrivacy')({ - component: DataPrivacyComponent, -}) - -function DataPrivacyComponent() { - return ( -
-

Step 1: User Information

-
- ) -} diff --git a/src/renderer/src/routes/Onboarding/DataPrivacy.tsx b/src/renderer/src/routes/Onboarding/DataPrivacy.tsx new file mode 100644 index 0000000..3864535 --- /dev/null +++ b/src/renderer/src/routes/Onboarding/DataPrivacy.tsx @@ -0,0 +1,73 @@ +import { styled } from '@mui/material/styles' +import { createFileRoute, useNavigate } from '@tanstack/react-router' + +import { Button } from '../../components/Button' +import { OnboardingTopMenu } from '../../components/OnboardingTopMenu' +import LockedIcon from '../../images/LockedWithKey.svg' + +export const Route = createFileRoute('/Onboarding/DataPrivacy')({ + component: DataPrivacyComponent, +}) + +const Container = styled('div')({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + backgroundColor: '#050F77', +}) + +const ContentBox = styled('div')({ + backgroundColor: '#fff', + borderRadius: '8px', + padding: '24px', + width: '80%', + textAlign: 'center', +}) + +const ButtonContainer = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + gap: '16px', + marginTop: '16px', +}) + +const StyledIcon = styled(LockedIcon)({ + marginBottom: '16px', +}) + +export function DataPrivacyComponent() { + const navigate = useNavigate() + + return ( + + + + ;

Review Data & Privacy

+

+ CoMapeo allows teams to map offline without needing internet servers. +

+
    +
  • Your data stays on your devices.
  • +
  • All data stays fully encrypted.
  • +
  • Easily manage and control sharing and collaboration.
  • +
  • + Private by default — diagnostic information is fully anonymized. +
  • +
+ + + + +
+
+ ) +} diff --git a/src/renderer/src/routes/Onboarding/NextStep.tsx b/src/renderer/src/routes/Onboarding/NextStep.tsx new file mode 100644 index 0000000..581b16e --- /dev/null +++ b/src/renderer/src/routes/Onboarding/NextStep.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/Onboarding/NextStep')({ + component: RouteComponent, +}) + +function RouteComponent() { + return 'Hello /Onboarding/NextStep!' +} diff --git a/src/renderer/src/routes/Onboarding.tsx b/src/renderer/src/routes/Onboarding/index.tsx similarity index 88% rename from src/renderer/src/routes/Onboarding.tsx rename to src/renderer/src/routes/Onboarding/index.tsx index ef1c48f..3968393 100644 --- a/src/renderer/src/routes/Onboarding.tsx +++ b/src/renderer/src/routes/Onboarding/index.tsx @@ -2,16 +2,16 @@ import { styled } from '@mui/material/styles' import { createFileRoute, useNavigate } from '@tanstack/react-router' import { defineMessages, useIntl } from 'react-intl' -import { CORNFLOWER_BLUE, DARK_COMAPEO_BLUE, ORANGE, WHITE } from '../colors' -import { Button } from '../components/Button' -import { Text } from '../components/Text' -import TopoBackground from '../images/TopoLogo.svg' -import Calling from '../images/calling.png' -import LockedWithKey from '../images/closed_lock_with_key.png' -import RaisedFistMediumSkinTone from '../images/raised_fist_medium_skin_tone.png' -import WorldMap from '../images/world_map.png' - -export const Route = createFileRoute('/Onboarding')({ +import { CORNFLOWER_BLUE, DARK_COMAPEO_BLUE, ORANGE, WHITE } from '../../colors' +import { Button } from '../../components/Button' +import { Text } from '../../components/Text' +import TopoBackground from '../../images/TopoLogo.svg' +import Calling from '../../images/calling.png' +import LockedWithKey from '../../images/closed_lock_with_key.png' +import RaisedFistMediumSkinTone from '../../images/raised_fist_medium_skin_tone.png' +import WorldMap from '../../images/world_map.png' + +export const Route = createFileRoute('/Onboarding/')({ component: OnboardingComponent, }) @@ -144,7 +144,7 @@ const StyledText = styled(Text)(({ theme }) => ({ fontSize: theme.typography.caption.fontSize, })) -function OnboardingComponent() { +export function OnboardingComponent() { const navigate = useNavigate() const { formatMessage } = useIntl() From e51e79e8bc8cae9343ac95ffc9d9d2e457bcc90b Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Wed, 27 Nov 2024 17:57:51 -0500 Subject: [PATCH 2/7] Data and Privacy Screen done. Working on Privacy Policy screen. --- messages/renderer/en.json | 132 ++++++++++++++ src/renderer/src/Theme.ts | 11 +- src/renderer/src/colors.ts | 2 + .../src/components/OnboardingTopMenu.tsx | 86 +++++++-- .../PrivacyPolicy/DiagnosticItem.tsx | 28 +++ .../PrivacyPolicy/PointContainer.tsx | 50 ++++++ .../src/components/PrivacyPolicy/index.tsx | 168 ++++++++++++++++++ .../PrivacyPolicy/privacyPolicyMessages.ts | 163 +++++++++++++++++ src/renderer/src/images/BustInSilhouette.svg | 2 + src/renderer/src/images/RedDot.svg | 1 + .../src/images/chevrondown-expanded.svg | 5 + src/renderer/src/images/chevrondown.svg | 5 + src/renderer/src/routeTree.gen.ts | 27 +++ .../src/routes/Onboarding/DataPrivacy.tsx | 139 ++++++++++++--- .../routes/Onboarding/PrivacyPolicyScreen.tsx | 41 +++++ 15 files changed, 820 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx create mode 100644 src/renderer/src/components/PrivacyPolicy/PointContainer.tsx create mode 100644 src/renderer/src/components/PrivacyPolicy/index.tsx create mode 100644 src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts create mode 100644 src/renderer/src/images/BustInSilhouette.svg create mode 100644 src/renderer/src/images/RedDot.svg create mode 100644 src/renderer/src/images/chevrondown-expanded.svg create mode 100644 src/renderer/src/images/chevrondown.svg create mode 100644 src/renderer/src/routes/Onboarding/PrivacyPolicyScreen.tsx diff --git a/messages/renderer/en.json b/messages/renderer/en.json index 839b3dc..d75f4cb 100644 --- a/messages/renderer/en.json +++ b/messages/renderer/en.json @@ -1,4 +1,28 @@ { + "screens.DataPrivacy.description": { + "message": "CoMapeo allows teams to map offline without needing internet servers." + }, + "screens.DataPrivacy.diagnostic": { + "message": "Private by default — diagnostic information is made fully anonymous and you can opt-out any time." + }, + "screens.DataPrivacy.encrypted": { + "message": "All data stays fully encrypted" + }, + "screens.DataPrivacy.learnMore": { + "message": "Learn More" + }, + "screens.DataPrivacy.manageAndControl": { + "message": "Easily manage and control sharing and collaboration." + }, + "screens.DataPrivacy.next": { + "message": "Next" + }, + "screens.DataPrivacy.stays": { + "message": "Your data stays on your devices." + }, + "screens.DataPrivacy.title": { + "message": "Review Data & Privacy" + }, "screens.IntroToCoMapeo.collaborate": { "message": "Collaborate with others" }, @@ -16,5 +40,113 @@ }, "screens.IntroToCoMapeo.viewAndManage": { "message": "View and manage observations in CoMapeo Mobile Projects." + }, + "screens.PrivacyPolicy.aboutAwana": { + "message": "About Awana Digital" + }, + "screens.PrivacyPolicy.aboutAwanaContent": { + "message": "CoMapeo is developed by Awana Digital, a 501c3 non-profit organization registered in the United States. Awana Digital works in solidarity with frontline communities to use technology to defend their rights and fight climate change." + }, + "screens.PrivacyPolicy.appErrors": { + "message": "App Errors" + }, + "screens.PrivacyPolicy.appErrorsDescription": { + "message": "Information about internal errors that result in the app not functioning as expected" + }, + "screens.PrivacyPolicy.appInfo": { + "message": "App Info" + }, + "screens.PrivacyPolicy.appInfoDescription": { + "message": "The version and locale (language) of CoMapeo" + }, + "screens.PrivacyPolicy.appUsageTitle": { + "message": "App Usage" + }, + "screens.PrivacyPolicy.comapeoPrivacyPolicy": { + "message": "CoMapeo Data Privacy" + }, + "screens.PrivacyPolicy.control": { + "message": "You're in control" + }, + "screens.PrivacyPolicy.controlDescription": { + "message": "You can opt out of sending any information to Awana Digital. You choose where your data is stored and who it is shared with. You may choose to share anonymized, summarized data about how you use CoMapeo with Awana Digital. We will always be transparent about what information you choose to share for the purposed of improving the app, and this information will never include photos, videos, audio, text, or precise locations that you have entered into CoMapeo." + }, + "screens.PrivacyPolicy.crashData": { + "message": "Crash Data" + }, + "screens.PrivacyPolicy.crashDataDescription": { + "message": "Information about what caused the app to close unexpectedly" + }, + "screens.PrivacyPolicy.dataCollection": { + "message": "CoMapeo Data Collection" + }, + "screens.PrivacyPolicy.deviceInfo": { + "message": "Device Info" + }, + "screens.PrivacyPolicy.deviceInfoDescription": { + "message": "Such as model and manufacturer of your device; device operating system; screen size; device locale (language); device memory." + }, + "screens.PrivacyPolicy.diagnosticsTitle": { + "message": "Diagnostics" + }, + "screens.PrivacyPolicy.noPII": { + "message": "No personally identifiable information" + }, + "screens.PrivacyPolicy.noPIIDescription": { + "message": "Using CoMapeo does not require a user account. Awana Digital does not collect your name, email address or any other personal details. No permanent user identifier or device identifier is ever shared with Awana Digital, and we take extra measures to ensure that no information you share can be used to track you: identifiers are randomized and rotated (changed) every month and we do not store IP addresses." + }, + "screens.PrivacyPolicy.notCollected": { + "message": "What is not collected?" + }, + "screens.PrivacyPolicy.notCollectedDescription": { + "message": "We do not collect any personal data or anything that can be used to identify or track a user or device. Device identifiers used to aggregate information are random, anonymous, and changed every month. Diagnostic information does not include data about how you use the app, and it never includes any of the data you have collected with the app. We do not collect your precise or coarse location, only the country where you are using CoMapeo." + }, + "screens.PrivacyPolicy.openSource": { + "message": "Open Source and the \"Official\" Version" + }, + "screens.PrivacyPolicy.openSourceContent": { + "message": "CoMapeo is an open-source application. This means that anyone can view the code that makes the app work and can verify the privacy declarations in this document. It also means that anyone can adapt the app to their own needs and release an alternative version. This document refers to data collected by the official releases of CoMapeo, digitally signed by Awana Digital, available from the Google Play Store or the Awana Digital website. Unofficial releases of CoMapeo obtained from other channels are outside our control and may share additional information with other organizations." + }, + "screens.PrivacyPolicy.overview": { + "message": "The following explains what information (or “data”) is sent from CoMapeo to the application developer, Awana Digital, and how that information is used." + }, + "screens.PrivacyPolicy.performanceData": { + "message": "Performance Data" + }, + "screens.PrivacyPolicy.performanceDataDescription": { + "message": "Such as launch time, energy usage, app freezes, and responsiveness" + }, + "screens.PrivacyPolicy.privacyPolicyTitle": { + "message": "Privacy Policy" + }, + "screens.PrivacyPolicy.privateByDefault": { + "message": "Private by default" + }, + "screens.PrivacyPolicy.privateByDefaultDescription": { + "message": "The data you collect and create with CoMapeo (locations, photos, video, audio, text) is only stored on your device by default, and is not stored or sent anywhere else. When you share data with collaborators by joining a project with them, it is sent encrypted, directly to your collaborators' device. This means that the data is not sent via Awana Digital, nor anyone else, on its way to your collaborator. Awana Digital never sees nor has access to any of the data you collect with CoMapeo unless you explicitly send it to us." + }, + "screens.PrivacyPolicy.thirdParty": { + "message": "Third-party access to data" + }, + "screens.PrivacyPolicy.thirdPartyDescription": { + "message": "A \"third-party\" is an organization other than Awana Digital." + }, + "screens.PrivacyPolicy.userCount": { + "message": "User Counts" + }, + "screens.PrivacyPolicy.userCountDescription": { + "message": "The number of users per country and per project. Aggregated and anonymized" + }, + "screens.PrivacyPolicy.whatIsCollected": { + "message": "What is collected?" + }, + "screens.PrivacyPolicy.whatIsCollectedDescription": { + "message": "By default, anonymized diagnostic information about your device, app crashes, errors and performance is shared with Awana Digital. You can opt-out of sharing this information at any time. This diagnostic information is completely anonymized and it never contains any of your data (the data you have collected with CoMapeo)." + }, + "screens.PrivacyPolicy.whyCollected": { + "message": "Why is this data collected?" + }, + "screens.PrivacyPolicy.whyCollectedDescription": { + "message": "Crash data and app errors together with the device and app info provide Awana Digital with the information we need to fix errors and bugs in the app. The performance data helps us improve the responsiveness of the app and identify errors. User counts, including total users, users per country, and users per project, help justify ongoing investment in the development of CoMapeo." } } diff --git a/src/renderer/src/Theme.ts b/src/renderer/src/Theme.ts index f6f0c8b..5201cbb 100644 --- a/src/renderer/src/Theme.ts +++ b/src/renderer/src/Theme.ts @@ -68,17 +68,20 @@ const theme = createTheme({ styleOverrides: { root: ({ ownerState }) => ({ ...(ownerState.variant === 'outlined' && { - borderColor: '#EEEEEE', - borderWidth: 2, + borderColor: '#CCCCD6', + borderWidth: 1, '&:hover': { - borderWidth: 2, + borderWidth: 1, }, '&:focus': { - borderWidth: 2, + borderWidth: 1, }, }), borderRadius: 4, textTransform: 'none', + fontSize: '1rem', + fontWeight: 400, + paddingVertical: 12, }), }, }, diff --git a/src/renderer/src/colors.ts b/src/renderer/src/colors.ts index 6dbad44..fad1f5a 100644 --- a/src/renderer/src/colors.ts +++ b/src/renderer/src/colors.ts @@ -4,6 +4,8 @@ export const LIGHT_COMAPEO_BLUE = '#CCE0FF' export const CORNFLOWER_BLUE = '#80A0FF' export const BLUE_GREY = '#CCCCD6' +export const DARK_GREY = '#757575' +export const VERY_LIGHT_GREY = '#ededed' export const ORANGE = '#FF9933' export const DARK_ORANGE = '#E86826' diff --git a/src/renderer/src/components/OnboardingTopMenu.tsx b/src/renderer/src/components/OnboardingTopMenu.tsx index c465e53..59bbd62 100644 --- a/src/renderer/src/components/OnboardingTopMenu.tsx +++ b/src/renderer/src/components/OnboardingTopMenu.tsx @@ -1,28 +1,60 @@ import { styled } from '@mui/material/styles' import { useNavigate } from '@tanstack/react-router' +import { BLACK, BLUE_GREY, COMAPEO_BLUE, WHITE } from '../colors' +import { Button } from './Button' +import { Text } from './Text' + const MenuContainer = styled('div')({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', - width: '90%', - marginBottom: '32px', - color: '#fff', + width: '55%', + margin: '16px auto', + position: 'relative', }) -const Steps = styled('div')({ +const GoBackButton = styled(Button)({ display: 'flex', alignItems: 'center', gap: '8px', + backgroundColor: 'transparent', + color: BLUE_GREY, + fontSize: '16px', + padding: '12px 32px', + borderRadius: '20px', + '&:hover': { + backgroundColor: 'rgba(0, 0, 0, 0.1)', + }, +}) + +const BackArrow = styled('span')({ + fontSize: 24, + color: WHITE, +}) + +const Steps = styled('div')({ + display: 'flex', + alignItems: 'center', + gap: 16, }) const Step = styled('div')(({ active }: { active: boolean }) => ({ - backgroundColor: active ? '#fff' : 'transparent', - color: active ? '#000' : '#fff', - padding: '4px 12px', - borderRadius: '16px', + backgroundColor: active ? WHITE : 'transparent', + color: active ? BLACK : BLUE_GREY, + padding: '12px 32px', + borderRadius: 20, + fontWeight: active ? 'bold' : 'normal', })) +const Divider = styled('div')({ + width: 16, + height: 1, + backgroundColor: COMAPEO_BLUE, + alignSelf: 'center', + margin: '0 12px', +}) + interface OnboardingTopMenuProps { currentStep: number } @@ -32,12 +64,42 @@ export function OnboardingTopMenu({ currentStep }: OnboardingTopMenuProps) { return ( - + navigate({ to: '/Onboarding' })} + variant="text" + style={{ + color: BLUE_GREY, + fontWeight: 500, + alignItems: 'center', + gap: 8, + backgroundColor: 'transparent', + padding: '12px 32px', + }} + > + + Go back + {['Step 1', 'Step 2', 'Step 3'].map((step, index) => ( - - {step} - +
+ + navigate({ + to: `/Onboarding/${ + index === 0 + ? 'DataPrivacy' // Example: current step + : 'FutureStep' // TODO: Replace with actual route + }`, + }) + } + > + + {step} + + + {index < 2 && } +
))}
diff --git a/src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx b/src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx new file mode 100644 index 0000000..bd82465 --- /dev/null +++ b/src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx @@ -0,0 +1,28 @@ +import MaterialIcons from '@mui/icons-material/FiberManualRecord' +import { styled } from '@mui/material/styles' + +import { Text } from '../../components/Text' + +const Container = styled('div')({ + display: 'flex', + alignItems: 'flex-start', + gap: '8px', +}) + +export const DiagnosticItem = ({ + title, + description, +}: { + title: string + description: string +}) => ( + + +
+ + {title}: + {' '} + {description} +
+
+) diff --git a/src/renderer/src/components/PrivacyPolicy/PointContainer.tsx b/src/renderer/src/components/PrivacyPolicy/PointContainer.tsx new file mode 100644 index 0000000..6adcc9e --- /dev/null +++ b/src/renderer/src/components/PrivacyPolicy/PointContainer.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { styled } from '@mui/material/styles' + +import { BLACK, DARK_GREY } from '../../colors' +import { Text } from '../Text' + +const Container = styled('div')({ + gap: 20, + paddingBottom: 20, + displau: 'flex', +}) + +const PointHeader = styled('div')({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 10, +}) + +const PointTitle = styled(Text)({ + fontSize: 16, + color: BLACK, +}) + +export function PointContainer({ + icon, + title, + description, +}: { + icon: React.ComponentType> | string + title: string + description: string +}) { + const isSvg = typeof icon !== 'string' + return ( + + + {isSvg && icon ? ( + React.createElement(icon, { width: 18, height: 18 }) + ) : ( + + )} + {title} + + + {description} + + + ) +} diff --git a/src/renderer/src/components/PrivacyPolicy/index.tsx b/src/renderer/src/components/PrivacyPolicy/index.tsx new file mode 100644 index 0000000..8a2cbcd --- /dev/null +++ b/src/renderer/src/components/PrivacyPolicy/index.tsx @@ -0,0 +1,168 @@ +import React from 'react' +import { styled } from '@mui/material/styles' +import { useIntl } from 'react-intl' + +import { BLUE_GREY, DARK_GREY, VERY_LIGHT_GREY, WHITE } from '../../colors' +import BustInSilhouette from '../../images/BustInSilhouette.svg' +import RedDot from '../../images/RedDot.svg' +import ChevronUp from '../../images/chevrondown-expanded.svg' +import ChevronDown from '../../images/chevrondown.svg' +import ClosedLockWithKey from '../../images/closed_lock_with_key.png' +import RaisedFistMediumSkinTone from '../../images/raised_fist_medium_skin_tone.png' +import { Text } from '../Text' +import { DiagnosticItem } from './DiagnosticItem' +import { PointContainer } from './PointContainer' +import { m } from './privacyPolicyMessages' + +const Container = styled('div')({ + display: 'flex', + flexDirection: 'column', + gap: '20px', + padding: '20px', + width: '100%', + maxWidth: '800px', + margin: '0 auto', + overflowY: 'auto', +}) + +const Header = styled(Text)({ + marginBottom: '16px', + textAlign: 'center', +}) + +const Subheader = styled(Text)({ + marginTop: 50, + fontSize: 24, + fontWeight: 'bold', + marginBottom: 30, +}) + +const ContentBox = styled('div')({ + width: '70%', + maxWidth: '800px', + margin: '0 auto', + padding: '20px', + backgroundColor: WHITE, + borderRadius: '10px', +}) + +const HorizontalLine = styled('div')({ + borderBottom: `1px solid ${BLUE_GREY}`, + margin: '20px 0', +}) + +const OverviewBox = styled('div')({ + padding: 20, + borderWidth: 1, + borderColor: BLUE_GREY, + borderRadius: 10, + backgroundColor: VERY_LIGHT_GREY, + marginBottom: 20, +}) + +const ToggleContainer = styled('div')( + ({ isTop, isBottom }: { isTop?: boolean; isBottom?: boolean }) => ({ + border: `1px solid ${BLUE_GREY}`, + borderRadius: isTop ? '10px 10px 0 0' : isBottom ? '0 0 10px 10px' : '0', + borderTopWidth: isBottom ? 0 : 1, + padding: '20px 0', + margin: 0, + gap: 'unset', + }), +) + +const ToggleHeader = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px 20px', + cursor: 'pointer', + backgroundColor: WHITE, +}) + +const ToggleContent = styled('div')({ + padding: '16px 20px', + backgroundColor: WHITE, +}) + +const ToggledText = styled(Text)(({ theme }) => ({ + color: DARK_GREY, + fontSize: theme.typography.body2.fontSize, +})) + +export function PrivacyPolicy() { + const { formatMessage } = useIntl() + const [awanaExpanded, setAwanaExpanded] = React.useState(false) + const [openSourceExpanded, setOpenSourceExpanded] = React.useState(false) + + return ( + +
{formatMessage(m.privacyPolicyTitle)}
+ + + {formatMessage(m.overview)} + + + setAwanaExpanded((prev) => !prev)}> + {formatMessage(m.aboutAwana)} + {awanaExpanded ? ( + + ) : ( + + )} + + {awanaExpanded && ( + + {formatMessage(m.aboutAwanaContent)} + + )} + + + setOpenSourceExpanded((prev) => !prev)}> + {formatMessage(m.openSource)} + {openSourceExpanded ? ( + + ) : ( + + )} + + {openSourceExpanded && ( + + {formatMessage(m.openSourceContent)} + + )} + + {formatMessage(m.comapeoPrivacyPolicy)} + + + + +
{formatMessage(m.diagnosticsTitle)}
+ + + +
+
+ ) +} diff --git a/src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts b/src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts new file mode 100644 index 0000000..12a2a60 --- /dev/null +++ b/src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts @@ -0,0 +1,163 @@ +import { defineMessages } from 'react-intl' + +export const m = defineMessages({ + overview: { + id: 'screens.PrivacyPolicy.overview', + defaultMessage: + 'The following explains what information (or “data”) is sent from CoMapeo to the application developer, Awana Digital, and how that information is used.', + }, + aboutAwana: { + id: 'screens.PrivacyPolicy.aboutAwana', + defaultMessage: 'About Awana Digital', + }, + aboutAwanaContent: { + id: 'screens.PrivacyPolicy.aboutAwanaContent', + defaultMessage: + 'CoMapeo is developed by Awana Digital, a 501c3 non-profit organization registered in the United States. Awana Digital works in solidarity with frontline communities to use technology to defend their rights and fight climate change.', + }, + privacyPolicyTitle: { + id: 'screens.PrivacyPolicy.privacyPolicyTitle', + defaultMessage: 'Privacy Policy', + }, + comapeoPrivacyPolicy: { + id: 'screens.PrivacyPolicy.comapeoPrivacyPolicy', + defaultMessage: 'CoMapeo Data Privacy', + }, + openSource: { + id: 'screens.PrivacyPolicy.openSource', + defaultMessage: 'Open Source and the "Official" Version', + }, + openSourceContent: { + id: 'screens.PrivacyPolicy.openSourceContent', + defaultMessage: + 'CoMapeo is an open-source application.\n\nThis means that anyone can view the code that makes the app work and can verify the privacy declarations in this document. It also means that anyone can adapt the app to their own needs and release an alternative version.\n\nThis document refers to data collected by the official releases of CoMapeo, digitally signed by Awana Digital, available from the Google Play Store or the Awana Digital website.\n\nUnofficial releases of CoMapeo obtained from other channels are outside our control and may share additional information with other organizations.', + }, + privateByDefault: { + id: 'screens.PrivacyPolicy.privateByDefault', + defaultMessage: 'Private by default', + }, + privateByDefaultDescription: { + id: 'screens.PrivacyPolicy.privateByDefaultDescription', + defaultMessage: + "The data you collect and create with CoMapeo (locations, photos, video, audio, text) is only stored on your device by default, and is not stored or sent anywhere else.\n\nWhen you share data with collaborators by joining a project with them, it is sent encrypted, directly to your collaborators' device. This means that the data is not sent via Awana Digital, nor anyone else, on its way to your collaborator.\n\nAwana Digital never sees nor has access to any of the data you collect with CoMapeo unless you explicitly send it to us.", + }, + noPII: { + id: 'screens.PrivacyPolicy.noPII', + defaultMessage: 'No personally identifiable information', + }, + noPIIDescription: { + id: 'screens.PrivacyPolicy.noPIIDescription', + defaultMessage: + 'Using CoMapeo does not require a user account.\n\nAwana Digital does not collect your name, email address or any other personal details.\n\nNo permanent user identifier or device identifier is ever shared with Awana Digital, and we take extra measures to ensure that no information you share can be used to track you: identifiers are randomized and rotated (changed) every month and we do not store IP addresses.', + }, + control: { + id: 'screens.PrivacyPolicy.control', + defaultMessage: "You're in control", + }, + controlDescription: { + id: 'screens.PrivacyPolicy.controlDescription', + defaultMessage: + 'You can opt out of sending any information to Awana Digital.\n\nYou choose where your data is stored and who it is shared with. You may choose to share anonymized, summarized data about how you use CoMapeo with Awana Digital.\n\nWe will always be transparent about what information you choose to share for the purposed of improving the app, and this information will never include photos, videos, audio, text, or precise locations that you have entered into CoMapeo.', + }, + dataCollection: { + id: 'screens.PrivacyPolicy.dataCollection', + defaultMessage: 'CoMapeo Data Collection', + }, + whatIsCollected: { + id: 'screens.PrivacyPolicy.whatIsCollected', + defaultMessage: 'What is collected?', + }, + whatIsCollectedDescription: { + id: 'screens.PrivacyPolicy.whatIsCollectedDescription', + defaultMessage: + 'By default, anonymized diagnostic information about your device, app crashes, errors and performance is shared with Awana Digital.\n\nYou can opt-out of sharing this information at any time. This diagnostic information is completely anonymized and it never contains any of your data (the data you have collected with CoMapeo).', + }, + diagnosticsTitle: { + id: 'screens.PrivacyPolicy.diagnosticsTitle', + defaultMessage: 'Diagnostics', + }, + crashData: { + id: 'screens.PrivacyPolicy.crashData', + defaultMessage: 'Crash Data', + }, + crashDataDescription: { + id: 'screens.PrivacyPolicy.crashDataDescription', + defaultMessage: + 'Information about what caused the app to close unexpectedly', + }, + appErrors: { + id: 'screens.PrivacyPolicy.appErrors', + defaultMessage: 'App Errors', + }, + appErrorsDescription: { + id: 'screens.PrivacyPolicy.appErrorsDescription', + defaultMessage: + 'Information about internal errors that result in the app not functioning as expected', + }, + performanceData: { + id: 'screens.PrivacyPolicy.performanceData', + defaultMessage: 'Performance Data', + }, + performanceDataDescription: { + id: 'screens.PrivacyPolicy.performanceDataDescription', + defaultMessage: + 'Such as launch time, energy usage, app freezes, and responsiveness', + }, + deviceInfo: { + id: 'screens.PrivacyPolicy.deviceInfo', + defaultMessage: 'Device Info', + }, + deviceInfoDescription: { + id: 'screens.PrivacyPolicy.deviceInfoDescription', + defaultMessage: + 'Such as model and manufacturer of your device; device operating system; screen size; device locale (language); device memory.', + }, + appInfo: { + id: 'screens.PrivacyPolicy.appInfo', + defaultMessage: 'App Info', + }, + appInfoDescription: { + id: 'screens.PrivacyPolicy.appInfoDescription', + defaultMessage: 'The version and locale (language) of CoMapeo', + }, + appUsageTitle: { + id: 'screens.PrivacyPolicy.appUsageTitle', + defaultMessage: 'App Usage', + }, + userCount: { + id: 'screens.PrivacyPolicy.userCount', + defaultMessage: 'User Counts', + }, + userCountDescription: { + id: 'screens.PrivacyPolicy.userCountDescription', + defaultMessage: + 'The number of users per country and per project. Aggregated and anonymized', + }, + whyCollected: { + id: 'screens.PrivacyPolicy.whyCollected', + defaultMessage: 'Why is this data collected?', + }, + whyCollectedDescription: { + id: 'screens.PrivacyPolicy.whyCollectedDescription', + defaultMessage: + 'Crash data and app errors together with the device and app info provide Awana Digital with the information we need to fix errors and bugs in the app. The performance data helps us improve the responsiveness of the app and identify errors. User counts, including total users, users per country, and users per project, help justify ongoing investment in the development of CoMapeo.', + }, + notCollected: { + id: 'screens.PrivacyPolicy.notCollected', + defaultMessage: 'What is not collected?', + }, + notCollectedDescription: { + id: 'screens.PrivacyPolicy.notCollectedDescription', + defaultMessage: + 'We do not collect any personal data or anything that can be used to identify or track a user or device. Device identifiers used to aggregate information are random, anonymous, and changed every month.\n\nDiagnostic information does not include data about how you use the app, and it never includes any of the data you have collected with the app. We do not collect your precise or coarse location, only the country where you are using CoMapeo.', + }, + thirdParty: { + id: 'screens.PrivacyPolicy.thirdParty', + defaultMessage: 'Third-party access to data', + }, + thirdPartyDescription: { + id: 'screens.PrivacyPolicy.thirdPartyDescription', + defaultMessage: + 'A "third-party" is an organization other than Awana Digital.', + }, +}) diff --git a/src/renderer/src/images/BustInSilhouette.svg b/src/renderer/src/images/BustInSilhouette.svg new file mode 100644 index 0000000..c5f2857 --- /dev/null +++ b/src/renderer/src/images/BustInSilhouette.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/renderer/src/images/RedDot.svg b/src/renderer/src/images/RedDot.svg new file mode 100644 index 0000000..bb5da97 --- /dev/null +++ b/src/renderer/src/images/RedDot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/images/chevrondown-expanded.svg b/src/renderer/src/images/chevrondown-expanded.svg new file mode 100644 index 0000000..86235ba --- /dev/null +++ b/src/renderer/src/images/chevrondown-expanded.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/renderer/src/images/chevrondown.svg b/src/renderer/src/images/chevrondown.svg new file mode 100644 index 0000000..384572e --- /dev/null +++ b/src/renderer/src/images/chevrondown.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/renderer/src/routeTree.gen.ts b/src/renderer/src/routeTree.gen.ts index 1ee939b..41e138c 100644 --- a/src/renderer/src/routeTree.gen.ts +++ b/src/renderer/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as MapTabsMapTab1Import } from './routes/(MapTabs)/_Map.tab1' import { Route as MapTabsMapTab2Import } from './routes/(MapTabs)/_Map.tab2' import { Route as OnboardingDataPrivacyImport } from './routes/Onboarding/DataPrivacy' import { Route as OnboardingNextStepImport } from './routes/Onboarding/NextStep' +import { Route as OnboardingPrivacyPolicyScreenImport } from './routes/Onboarding/PrivacyPolicyScreen' import { Route as OnboardingIndexImport } from './routes/Onboarding/index' import { Route as WelcomeImport } from './routes/Welcome' // Import Routes @@ -51,6 +52,13 @@ const OnboardingIndexRoute = OnboardingIndexImport.update({ getParentRoute: () => rootRoute, } as any) +const OnboardingPrivacyPolicyScreenRoute = + OnboardingPrivacyPolicyScreenImport.update({ + id: '/Onboarding/PrivacyPolicyScreen', + path: '/Onboarding/PrivacyPolicyScreen', + getParentRoute: () => rootRoute, + } as any) + const OnboardingNextStepRoute = OnboardingNextStepImport.update({ id: '/Onboarding/NextStep', path: '/Onboarding/NextStep', @@ -126,6 +134,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingNextStepImport parentRoute: typeof rootRoute } + '/Onboarding/PrivacyPolicyScreen': { + id: '/Onboarding/PrivacyPolicyScreen' + path: '/Onboarding/PrivacyPolicyScreen' + fullPath: '/Onboarding/PrivacyPolicyScreen' + preLoaderRoute: typeof OnboardingPrivacyPolicyScreenImport + parentRoute: typeof rootRoute + } '/Onboarding/': { id: '/Onboarding/' path: '/Onboarding' @@ -182,6 +197,7 @@ export interface FileRoutesByFullPath { '/Welcome': typeof WelcomeRoute '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute '/Onboarding': typeof OnboardingIndexRoute '/tab1': typeof MapTabsMapTab1Route '/tab2': typeof MapTabsMapTab2Route @@ -192,6 +208,7 @@ export interface FileRoutesByTo { '/Welcome': typeof WelcomeRoute '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute '/Onboarding': typeof OnboardingIndexRoute '/tab1': typeof MapTabsMapTab1Route '/tab2': typeof MapTabsMapTab2Route @@ -205,6 +222,7 @@ export interface FileRoutesById { '/(MapTabs)/_Map': typeof MapTabsMapRouteWithChildren '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute '/Onboarding/': typeof OnboardingIndexRoute '/(MapTabs)/_Map/tab1': typeof MapTabsMapTab1Route '/(MapTabs)/_Map/tab2': typeof MapTabsMapTab2Route @@ -217,6 +235,7 @@ export interface FileRouteTypes { | '/Welcome' | '/Onboarding/DataPrivacy' | '/Onboarding/NextStep' + | '/Onboarding/PrivacyPolicyScreen' | '/Onboarding' | '/tab1' | '/tab2' @@ -226,6 +245,7 @@ export interface FileRouteTypes { | '/Welcome' | '/Onboarding/DataPrivacy' | '/Onboarding/NextStep' + | '/Onboarding/PrivacyPolicyScreen' | '/Onboarding' | '/tab1' | '/tab2' @@ -237,6 +257,7 @@ export interface FileRouteTypes { | '/(MapTabs)/_Map' | '/Onboarding/DataPrivacy' | '/Onboarding/NextStep' + | '/Onboarding/PrivacyPolicyScreen' | '/Onboarding/' | '/(MapTabs)/_Map/tab1' | '/(MapTabs)/_Map/tab2' @@ -249,6 +270,7 @@ export interface RootRouteChildren { MapTabsRoute: typeof MapTabsRouteWithChildren OnboardingDataPrivacyRoute: typeof OnboardingDataPrivacyRoute OnboardingNextStepRoute: typeof OnboardingNextStepRoute + OnboardingPrivacyPolicyScreenRoute: typeof OnboardingPrivacyPolicyScreenRoute OnboardingIndexRoute: typeof OnboardingIndexRoute } @@ -258,6 +280,7 @@ const rootRouteChildren: RootRouteChildren = { MapTabsRoute: MapTabsRouteWithChildren, OnboardingDataPrivacyRoute: OnboardingDataPrivacyRoute, OnboardingNextStepRoute: OnboardingNextStepRoute, + OnboardingPrivacyPolicyScreenRoute: OnboardingPrivacyPolicyScreenRoute, OnboardingIndexRoute: OnboardingIndexRoute, } @@ -276,6 +299,7 @@ export const routeTree = rootRoute "/(MapTabs)", "/Onboarding/DataPrivacy", "/Onboarding/NextStep", + "/Onboarding/PrivacyPolicyScreen", "/Onboarding/" ] }, @@ -305,6 +329,9 @@ export const routeTree = rootRoute "/Onboarding/NextStep": { "filePath": "Onboarding/NextStep.tsx" }, + "/Onboarding/PrivacyPolicyScreen": { + "filePath": "Onboarding/PrivacyPolicyScreen.tsx" + }, "/Onboarding/": { "filePath": "Onboarding/index.tsx" }, diff --git a/src/renderer/src/routes/Onboarding/DataPrivacy.tsx b/src/renderer/src/routes/Onboarding/DataPrivacy.tsx index 3864535..6faad02 100644 --- a/src/renderer/src/routes/Onboarding/DataPrivacy.tsx +++ b/src/renderer/src/routes/Onboarding/DataPrivacy.tsx @@ -1,10 +1,56 @@ import { styled } from '@mui/material/styles' import { createFileRoute, useNavigate } from '@tanstack/react-router' +import { defineMessages, useIntl } from 'react-intl' +import { + BLACK, + BLUE_GREY, + DARK_COMAPEO_BLUE, + DARK_GREY, + WHITE, +} from '../../colors' import { Button } from '../../components/Button' import { OnboardingTopMenu } from '../../components/OnboardingTopMenu' +import { Text } from '../../components/Text' import LockedIcon from '../../images/LockedWithKey.svg' +export const m = defineMessages({ + title: { + id: 'screens.DataPrivacy.title', + defaultMessage: 'Review Data & Privacy', + }, + description: { + id: 'screens.DataPrivacy.description', + defaultMessage: + 'CoMapeo allows teams to map offline without needing internet servers.', + }, + dataPrivacyStays: { + id: 'screens.DataPrivacy.stays', + defaultMessage: 'Your data stays on your devices.', + }, + dataPrivacyEncrypted: { + id: 'screens.DataPrivacy.encrypted', + defaultMessage: 'All data stays fully encrypted', + }, + dataPrivacyManageAndControl: { + id: 'screens.DataPrivacy.manageAndControl', + defaultMessage: 'Easily manage and control sharing and collaboration.', + }, + dataPrivacyDiagnostic: { + id: 'screens.DataPrivacy.diagnostic', + defaultMessage: + 'Private by default — diagnostic information is made fully anonymous and you can opt-out any time.', + }, + learnMore: { + id: 'screens.DataPrivacy.learnMore', + defaultMessage: 'Learn More', + }, + next: { + id: 'screens.DataPrivacy.next', + defaultMessage: 'Next', + }, +}) + export const Route = createFileRoute('/Onboarding/DataPrivacy')({ component: DataPrivacyComponent, }) @@ -14,57 +60,102 @@ const Container = styled('div')({ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - height: '100vh', - backgroundColor: '#050F77', + height: '100%', + backgroundColor: DARK_COMAPEO_BLUE, }) const ContentBox = styled('div')({ - backgroundColor: '#fff', - borderRadius: '8px', - padding: '24px', - width: '80%', + backgroundColor: 'rgba(255, 255, 255, 0.94)', + border: `1px solid ${BLUE_GREY}`, + borderRadius: 8, + padding: 20, + width: '55%', + textAlign: 'center', + boxShadow: '0px 4px 4px 0px rgba(0, 0, 0, 0.02)', +}) + +const BodyTextWrapper = styled('div')({ + maxWidth: '40%', + margin: '16px auto 0', textAlign: 'center', }) const ButtonContainer = styled('div')({ display: 'flex', justifyContent: 'space-between', - gap: '16px', - marginTop: '16px', + gap: 15, + marginTop: 63, + padding: '0 20px', }) const StyledIcon = styled(LockedIcon)({ marginBottom: '16px', + width: 40, + height: 50, +}) + +const BulletList = styled('ul')({ + width: '50%', + textAlign: 'left', + margin: '16px auto', + color: DARK_GREY, + paddingLeft: 0, +}) + +const BulletListItem = styled('li')({ + marginBottom: 8, }) export function DataPrivacyComponent() { const navigate = useNavigate() + const { formatMessage } = useIntl() return ( - ;

Review Data & Privacy

-

- CoMapeo allows teams to map offline without needing internet servers. -

-
    -
  • Your data stays on your devices.
  • -
  • All data stays fully encrypted.
  • -
  • Easily manage and control sharing and collaboration.
  • -
  • - Private by default — diagnostic information is fully anonymized. -
  • -
+ + {formatMessage(m.title)} + + + {formatMessage(m.description)} + + + + + {formatMessage(m.dataPrivacyStays)} + + + {formatMessage(m.dataPrivacyEncrypted)} + + + + {formatMessage(m.dataPrivacyManageAndControl)} + + + + {formatMessage(m.dataPrivacyDiagnostic)} + + -
diff --git a/src/renderer/src/routes/Onboarding/PrivacyPolicyScreen.tsx b/src/renderer/src/routes/Onboarding/PrivacyPolicyScreen.tsx new file mode 100644 index 0000000..f92f975 --- /dev/null +++ b/src/renderer/src/routes/Onboarding/PrivacyPolicyScreen.tsx @@ -0,0 +1,41 @@ +import { styled } from '@mui/material/styles' +import { createFileRoute } from '@tanstack/react-router' + +import { BLUE_GREY, DARK_COMAPEO_BLUE } from '../../colors' +import { OnboardingTopMenu } from '../../components/OnboardingTopMenu' +import { PrivacyPolicy } from '../../components/PrivacyPolicy' + +export const Route = createFileRoute('/Onboarding/PrivacyPolicyScreen')({ + component: PrivacyPolicyScreen, +}) + +const Container = styled('div')({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + backgroundColor: DARK_COMAPEO_BLUE, +}) +const ContentBox = styled('div')({ + width: '55%', + height: '80%', + backgroundColor: '#FFFFFF', + padding: '20px 30px', + borderRadius: 8, + border: `1px solid ${BLUE_GREY}`, + margin: '16px auto', + boxShadow: '0px 4px 4px 0px rgba(0, 0, 0, 0.02)', + overflowY: 'auto', +}) + +export function PrivacyPolicyScreen() { + return ( + + + + + + + ) +} From c537431cd93c3071439c1fc8d11218a64effbc94 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 2 Dec 2024 17:30:24 -0500 Subject: [PATCH 3/7] The rest of the UI for the privacy policy screen for onboarding. --- messages/renderer/en.json | 6 + .../PrivacyPolicy/DiagnosticItem.tsx | 38 ++++-- .../MetricsDiagnosticsPermissionToggle.tsx | 51 ++++++++ .../src/components/PrivacyPolicy/index.tsx | 115 ++++++++++++++---- .../PrivacyPolicy/privacyPolicyMessages.ts | 4 + src/renderer/src/images/BarChart.svg | 2 + src/renderer/src/images/Wrench.svg | 2 + .../images/raised_hand_medium_skin_tone.png | Bin 0 -> 8580 bytes 8 files changed, 180 insertions(+), 38 deletions(-) create mode 100644 src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx create mode 100644 src/renderer/src/images/BarChart.svg create mode 100644 src/renderer/src/images/Wrench.svg create mode 100644 src/renderer/src/images/raised_hand_medium_skin_tone.png diff --git a/messages/renderer/en.json b/messages/renderer/en.json index d75f4cb..58703ad 100644 --- a/messages/renderer/en.json +++ b/messages/renderer/en.json @@ -41,6 +41,9 @@ "screens.IntroToCoMapeo.viewAndManage": { "message": "View and manage observations in CoMapeo Mobile Projects." }, + "screens.OnboardingPrivacyPolicy.permissionsTitle": { + "message": "Current Permissions" + }, "screens.PrivacyPolicy.aboutAwana": { "message": "About Awana Digital" }, @@ -125,6 +128,9 @@ "screens.PrivacyPolicy.privateByDefaultDescription": { "message": "The data you collect and create with CoMapeo (locations, photos, video, audio, text) is only stored on your device by default, and is not stored or sent anywhere else. When you share data with collaborators by joining a project with them, it is sent encrypted, directly to your collaborators' device. This means that the data is not sent via Awana Digital, nor anyone else, on its way to your collaborator. Awana Digital never sees nor has access to any of the data you collect with CoMapeo unless you explicitly send it to us." }, + "screens.PrivacyPolicy.shareDiagnostics": { + "message": "Share Diagnostic Information" + }, "screens.PrivacyPolicy.thirdParty": { "message": "Third-party access to data" }, diff --git a/src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx b/src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx index bd82465..332524b 100644 --- a/src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx +++ b/src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx @@ -1,12 +1,25 @@ -import MaterialIcons from '@mui/icons-material/FiberManualRecord' +import CircleIcon from '@mui/icons-material/Circle' import { styled } from '@mui/material/styles' +import { DARK_GREY } from '../../colors' import { Text } from '../../components/Text' -const Container = styled('div')({ +const DiagnosticsItem = styled('div')({ display: 'flex', + flexDirection: 'row', alignItems: 'flex-start', - gap: '8px', + gap: 8, + marginBottom: 12, + paddingLeft: 12, +}) +const DiagnosticsTitle = styled(Text)({ + fontWeight: 'bold', + marginRight: 4, + color: DARK_GREY, + lineHeight: 1, +}) +const DiagnosticsDescription = styled(Text)({ + display: 'inline', }) export const DiagnosticItem = ({ @@ -16,13 +29,14 @@ export const DiagnosticItem = ({ title: string description: string }) => ( - - -
- - {title}: - {' '} - {description} -
-
+ + + + {title}:{' '} + {description} + + ) diff --git a/src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx b/src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx new file mode 100644 index 0000000..cf9bf7e --- /dev/null +++ b/src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import Checkbox from '@mui/material/Checkbox' +import { styled } from '@mui/material/styles' +import { defineMessages, useIntl } from 'react-intl' + +import { BLACK } from '../../colors' + +const m = defineMessages({ + shareDiagnostics: { + id: 'screens.PrivacyPolicy.shareDiagnostics', + defaultMessage: 'Share Diagnostic Information', + }, +}) + +const Container = styled('div')({ + display: 'flex', + alignItems: 'center', +}) + +const PermissionText = styled('span')({ + fontSize: 16, + color: BLACK, + flex: 1, +}) + +export const MetricsDiagnosticsPermissionToggle = () => { + const { formatMessage } = useIntl() + const [isEnabled, setIsEnabled] = React.useState(() => { + const savedValue = localStorage.getItem('MetricDiagnosticsPermission') + return savedValue !== null ? savedValue === 'true' : true + }) + + const togglePermission = () => { + const newValue = !isEnabled + setIsEnabled(newValue) + localStorage.setItem('MetricDiagnosticsPermission', String(newValue)) + + console.log('Permission updated:', newValue) + } + + return ( + + {formatMessage(m.shareDiagnostics)} + + + ) +} diff --git a/src/renderer/src/components/PrivacyPolicy/index.tsx b/src/renderer/src/components/PrivacyPolicy/index.tsx index 8a2cbcd..ceb9d35 100644 --- a/src/renderer/src/components/PrivacyPolicy/index.tsx +++ b/src/renderer/src/components/PrivacyPolicy/index.tsx @@ -3,54 +3,52 @@ import { styled } from '@mui/material/styles' import { useIntl } from 'react-intl' import { BLUE_GREY, DARK_GREY, VERY_LIGHT_GREY, WHITE } from '../../colors' +import BarChart from '../../images/BarChart.svg' import BustInSilhouette from '../../images/BustInSilhouette.svg' import RedDot from '../../images/RedDot.svg' +import Wrench from '../../images/Wrench.svg' import ChevronUp from '../../images/chevrondown-expanded.svg' import ChevronDown from '../../images/chevrondown.svg' import ClosedLockWithKey from '../../images/closed_lock_with_key.png' import RaisedFistMediumSkinTone from '../../images/raised_fist_medium_skin_tone.png' +import RaisedHandMediumSkinTone from '../../images/raised_hand_medium_skin_tone.png' import { Text } from '../Text' import { DiagnosticItem } from './DiagnosticItem' +import { MetricsDiagnosticsPermissionToggle } from './MetricsDiagnosticsPermissionToggle' import { PointContainer } from './PointContainer' import { m } from './privacyPolicyMessages' const Container = styled('div')({ display: 'flex', flexDirection: 'column', - gap: '20px', - padding: '20px', + gap: 20, + padding: 20, width: '100%', - maxWidth: '800px', + maxWidth: 800, margin: '0 auto', overflowY: 'auto', }) - const Header = styled(Text)({ - marginBottom: '16px', textAlign: 'center', }) - const Subheader = styled(Text)({ marginTop: 50, fontSize: 24, fontWeight: 'bold', marginBottom: 30, }) - const ContentBox = styled('div')({ width: '70%', - maxWidth: '800px', + maxWidth: 800, margin: '0 auto', - padding: '20px', + padding: 20, backgroundColor: WHITE, - borderRadius: '10px', + borderRadius: 10, }) - const HorizontalLine = styled('div')({ borderBottom: `1px solid ${BLUE_GREY}`, margin: '20px 0', }) - const OverviewBox = styled('div')({ padding: 20, borderWidth: 1, @@ -59,7 +57,6 @@ const OverviewBox = styled('div')({ backgroundColor: VERY_LIGHT_GREY, marginBottom: 20, }) - const ToggleContainer = styled('div')( ({ isTop, isBottom }: { isTop?: boolean; isBottom?: boolean }) => ({ border: `1px solid ${BLUE_GREY}`, @@ -70,7 +67,6 @@ const ToggleContainer = styled('div')( gap: 'unset', }), ) - const ToggleHeader = styled('div')({ display: 'flex', justifyContent: 'space-between', @@ -79,16 +75,35 @@ const ToggleHeader = styled('div')({ cursor: 'pointer', backgroundColor: WHITE, }) - const ToggleContent = styled('div')({ padding: '16px 20px', backgroundColor: WHITE, }) - const ToggledText = styled(Text)(({ theme }) => ({ color: DARK_GREY, fontSize: theme.typography.body2.fontSize, })) +const DiagnosticsContainer = styled('div')({ + border: `1px solid ${BLUE_GREY}`, + borderRadius: 10, + marginBottom: 20, + paddingBottom: 8, +}) +const DiagnosticsContent = styled('div')({ + paddingLeft: 20, +}) +const HorizontalLineSmall = styled('div')({ + borderBottom: `1px solid ${BLUE_GREY}`, + margin: '20px 20px 20px 0', +}) +const PermissionToggleContainer = styled('div')({ + flexDirection: 'row', + alignItems: 'center', + backgroundColor: WHITE, + padding: 15, + border: `1px solid ${BLUE_GREY}`, + borderRadius: 10, +}) export function PrivacyPolicy() { const { formatMessage } = useIntl() @@ -149,19 +164,67 @@ export function PrivacyPolicy() { description={formatMessage(m.controlDescription)} /> -
{formatMessage(m.diagnosticsTitle)}
- {formatMessage(m.dataCollection)} + - + + {formatMessage(m.diagnosticsTitle)} + + + + + + + + + + {formatMessage(m.appUsageTitle)} + + + + + - + + + {formatMessage(m.permissionsTitle)} + + +
) diff --git a/src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts b/src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts index 12a2a60..7b2251b 100644 --- a/src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts +++ b/src/renderer/src/components/PrivacyPolicy/privacyPolicyMessages.ts @@ -160,4 +160,8 @@ export const m = defineMessages({ defaultMessage: 'A "third-party" is an organization other than Awana Digital.', }, + permissionsTitle: { + id: 'screens.OnboardingPrivacyPolicy.permissionsTitle', + defaultMessage: 'Current Permissions', + }, }) diff --git a/src/renderer/src/images/BarChart.svg b/src/renderer/src/images/BarChart.svg new file mode 100644 index 0000000..6fe59a5 --- /dev/null +++ b/src/renderer/src/images/BarChart.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/renderer/src/images/Wrench.svg b/src/renderer/src/images/Wrench.svg new file mode 100644 index 0000000..8656b0d --- /dev/null +++ b/src/renderer/src/images/Wrench.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/renderer/src/images/raised_hand_medium_skin_tone.png b/src/renderer/src/images/raised_hand_medium_skin_tone.png new file mode 100644 index 0000000000000000000000000000000000000000..0c46823df772a7eada368bd4f45c33160271e774 GIT binary patch literal 8580 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 KMNUMnLSTY=E^u!E literal 0 HcmV?d00001 From 84114ab5b326eac9ff106d62d1a8916202b3baa8 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 2 Dec 2024 18:18:31 -0500 Subject: [PATCH 4/7] Fixes the top menu. --- messages/renderer/en.json | 6 +++ .../src/components/OnboardingTopMenu.tsx | 49 ++++++++++++------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/messages/renderer/en.json b/messages/renderer/en.json index 58703ad..2ecee9c 100644 --- a/messages/renderer/en.json +++ b/messages/renderer/en.json @@ -1,4 +1,10 @@ { + "components.OnboardingTopMenu.goBack": { + "message": "Go back" + }, + "components.OnboardingTopMenu.step": { + "message": "Step {number}" + }, "screens.DataPrivacy.description": { "message": "CoMapeo allows teams to map offline without needing internet servers." }, diff --git a/src/renderer/src/components/OnboardingTopMenu.tsx b/src/renderer/src/components/OnboardingTopMenu.tsx index 59bbd62..6d51320 100644 --- a/src/renderer/src/components/OnboardingTopMenu.tsx +++ b/src/renderer/src/components/OnboardingTopMenu.tsx @@ -1,5 +1,6 @@ import { styled } from '@mui/material/styles' -import { useNavigate } from '@tanstack/react-router' +import { useNavigate, useRouter } from '@tanstack/react-router' +import { defineMessages, useIntl } from 'react-intl' import { BLACK, BLUE_GREY, COMAPEO_BLUE, WHITE } from '../colors' import { Button } from './Button' @@ -17,12 +18,13 @@ const MenuContainer = styled('div')({ const GoBackButton = styled(Button)({ display: 'flex', alignItems: 'center', - gap: '8px', + gap: 8, backgroundColor: 'transparent', color: BLUE_GREY, - fontSize: '16px', + fontSize: 16, padding: '12px 32px', - borderRadius: '20px', + borderRadius: 20, + whiteSpace: 'nowrap', '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.1)', }, @@ -45,6 +47,7 @@ const Step = styled('div')(({ active }: { active: boolean }) => ({ padding: '12px 32px', borderRadius: 20, fontWeight: active ? 'bold' : 'normal', + whiteSpace: 'nowrap', })) const Divider = styled('div')({ @@ -59,46 +62,58 @@ interface OnboardingTopMenuProps { currentStep: number } +const m = defineMessages({ + goBack: { + id: 'components.OnboardingTopMenu.goBack', + defaultMessage: 'Go back', + }, + step: { + id: 'components.OnboardingTopMenu.step', + defaultMessage: 'Step {number}', + }, +}) + export function OnboardingTopMenu({ currentStep }: OnboardingTopMenuProps) { const navigate = useNavigate() + const router = useRouter() + const { formatMessage } = useIntl() return ( navigate({ to: '/Onboarding' })} + onClick={() => router.history.back()} variant="text" style={{ color: BLUE_GREY, - fontWeight: 500, - alignItems: 'center', gap: 8, - backgroundColor: 'transparent', padding: '12px 32px', }} > - Go back + {formatMessage(m.goBack)} - {['Step 1', 'Step 2', 'Step 3'].map((step, index) => ( + {[1, 2, 3].map((step) => (
navigate({ to: `/Onboarding/${ - index === 0 - ? 'DataPrivacy' // Example: current step - : 'FutureStep' // TODO: Replace with actual route + step === 1 + ? 'DataPrivacy' + : step === 2 + ? 'NextStep' + : 'PrivacyPolicyScreen' }`, }) } > - - {step} + + {formatMessage(m.step, { number: step })} - {index < 2 && } + {step < 3 && }
))}
From cbcb83f42cf9273adac871a8fc635b4a22f3a5bd Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 2 Dec 2024 18:49:48 -0500 Subject: [PATCH 5/7] Some linting prettier fixes. --- .prettierignore | 1 + eslint.config.js | 1 + src/renderer/src/routeTree.gen.ts | 388 +++++++++--------- .../src/routes/Onboarding/NextStep.tsx | 1 - 4 files changed, 196 insertions(+), 195 deletions(-) diff --git a/.prettierignore b/.prettierignore index 1807c4b..91e6c9a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ /data /messages /translations +src/renderer/src/routeTree.gen.ts diff --git a/eslint.config.js b/eslint.config.js index a52025c..a41e72e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -77,6 +77,7 @@ export default tseslint.config( ecmaVersion: 2022, sourceType: 'module', }, + ignores: ['src/renderer/src/routeTree.gen.ts'], }, // Global ignores includeIgnoreFile(fileURLToPath(new URL('.gitignore', import.meta.url))), diff --git a/src/renderer/src/routeTree.gen.ts b/src/renderer/src/routeTree.gen.ts index 41e138c..3207835 100644 --- a/src/renderer/src/routeTree.gen.ts +++ b/src/renderer/src/routeTree.gen.ts @@ -10,18 +10,18 @@ import { createFileRoute } from '@tanstack/react-router' -import { Route as MapTabsMapImport } from './routes/(MapTabs)/_Map' -import { Route as MapTabsMapTab1Import } from './routes/(MapTabs)/_Map.tab1' -import { Route as MapTabsMapTab2Import } from './routes/(MapTabs)/_Map.tab2' -import { Route as OnboardingDataPrivacyImport } from './routes/Onboarding/DataPrivacy' -import { Route as OnboardingNextStepImport } from './routes/Onboarding/NextStep' -import { Route as OnboardingPrivacyPolicyScreenImport } from './routes/Onboarding/PrivacyPolicyScreen' -import { Route as OnboardingIndexImport } from './routes/Onboarding/index' -import { Route as WelcomeImport } from './routes/Welcome' // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as WelcomeImport } from './routes/Welcome' import { Route as IndexImport } from './routes/index' +import { Route as OnboardingIndexImport } from './routes/Onboarding/index' +import { Route as OnboardingPrivacyPolicyScreenImport } from './routes/Onboarding/PrivacyPolicyScreen' +import { Route as OnboardingNextStepImport } from './routes/Onboarding/NextStep' +import { Route as OnboardingDataPrivacyImport } from './routes/Onboarding/DataPrivacy' +import { Route as MapTabsMapImport } from './routes/(MapTabs)/_Map' +import { Route as MapTabsMapTab2Import } from './routes/(MapTabs)/_Map.tab2' +import { Route as MapTabsMapTab1Import } from './routes/(MapTabs)/_Map.tab1' // Create Virtual Routes @@ -30,263 +30,263 @@ const MapTabsImport = createFileRoute('/(MapTabs)')() // Create/Update Routes const MapTabsRoute = MapTabsImport.update({ - id: '/(MapTabs)', - getParentRoute: () => rootRoute, + id: '/(MapTabs)', + getParentRoute: () => rootRoute, } as any) const WelcomeRoute = WelcomeImport.update({ - id: '/Welcome', - path: '/Welcome', - getParentRoute: () => rootRoute, + id: '/Welcome', + path: '/Welcome', + getParentRoute: () => rootRoute, } as any) const IndexRoute = IndexImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRoute, + id: '/', + path: '/', + getParentRoute: () => rootRoute, } as any) const OnboardingIndexRoute = OnboardingIndexImport.update({ - id: '/Onboarding/', - path: '/Onboarding/', - getParentRoute: () => rootRoute, + id: '/Onboarding/', + path: '/Onboarding/', + getParentRoute: () => rootRoute, } as any) const OnboardingPrivacyPolicyScreenRoute = - OnboardingPrivacyPolicyScreenImport.update({ - id: '/Onboarding/PrivacyPolicyScreen', - path: '/Onboarding/PrivacyPolicyScreen', - getParentRoute: () => rootRoute, - } as any) + OnboardingPrivacyPolicyScreenImport.update({ + id: '/Onboarding/PrivacyPolicyScreen', + path: '/Onboarding/PrivacyPolicyScreen', + getParentRoute: () => rootRoute, + } as any) const OnboardingNextStepRoute = OnboardingNextStepImport.update({ - id: '/Onboarding/NextStep', - path: '/Onboarding/NextStep', - getParentRoute: () => rootRoute, + id: '/Onboarding/NextStep', + path: '/Onboarding/NextStep', + getParentRoute: () => rootRoute, } as any) const OnboardingDataPrivacyRoute = OnboardingDataPrivacyImport.update({ - id: '/Onboarding/DataPrivacy', - path: '/Onboarding/DataPrivacy', - getParentRoute: () => rootRoute, + id: '/Onboarding/DataPrivacy', + path: '/Onboarding/DataPrivacy', + getParentRoute: () => rootRoute, } as any) const MapTabsMapRoute = MapTabsMapImport.update({ - id: '/_Map', - getParentRoute: () => MapTabsRoute, + id: '/_Map', + getParentRoute: () => MapTabsRoute, } as any) const MapTabsMapTab2Route = MapTabsMapTab2Import.update({ - id: '/tab2', - path: '/tab2', - getParentRoute: () => MapTabsMapRoute, + id: '/tab2', + path: '/tab2', + getParentRoute: () => MapTabsMapRoute, } as any) const MapTabsMapTab1Route = MapTabsMapTab1Import.update({ - id: '/tab1', - path: '/tab1', - getParentRoute: () => MapTabsMapRoute, + id: '/tab1', + path: '/tab1', + getParentRoute: () => MapTabsMapRoute, } as any) // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } - '/Welcome': { - id: '/Welcome' - path: '/Welcome' - fullPath: '/Welcome' - preLoaderRoute: typeof WelcomeImport - parentRoute: typeof rootRoute - } - '/(MapTabs)': { - id: '/(MapTabs)' - path: '/' - fullPath: '/' - preLoaderRoute: typeof MapTabsImport - parentRoute: typeof rootRoute - } - '/(MapTabs)/_Map': { - id: '/(MapTabs)/_Map' - path: '/' - fullPath: '/' - preLoaderRoute: typeof MapTabsMapImport - parentRoute: typeof MapTabsRoute - } - '/Onboarding/DataPrivacy': { - id: '/Onboarding/DataPrivacy' - path: '/Onboarding/DataPrivacy' - fullPath: '/Onboarding/DataPrivacy' - preLoaderRoute: typeof OnboardingDataPrivacyImport - parentRoute: typeof rootRoute - } - '/Onboarding/NextStep': { - id: '/Onboarding/NextStep' - path: '/Onboarding/NextStep' - fullPath: '/Onboarding/NextStep' - preLoaderRoute: typeof OnboardingNextStepImport - parentRoute: typeof rootRoute - } - '/Onboarding/PrivacyPolicyScreen': { - id: '/Onboarding/PrivacyPolicyScreen' - path: '/Onboarding/PrivacyPolicyScreen' - fullPath: '/Onboarding/PrivacyPolicyScreen' - preLoaderRoute: typeof OnboardingPrivacyPolicyScreenImport - parentRoute: typeof rootRoute - } - '/Onboarding/': { - id: '/Onboarding/' - path: '/Onboarding' - fullPath: '/Onboarding' - preLoaderRoute: typeof OnboardingIndexImport - parentRoute: typeof rootRoute - } - '/(MapTabs)/_Map/tab1': { - id: '/(MapTabs)/_Map/tab1' - path: '/tab1' - fullPath: '/tab1' - preLoaderRoute: typeof MapTabsMapTab1Import - parentRoute: typeof MapTabsMapImport - } - '/(MapTabs)/_Map/tab2': { - id: '/(MapTabs)/_Map/tab2' - path: '/tab2' - fullPath: '/tab2' - preLoaderRoute: typeof MapTabsMapTab2Import - parentRoute: typeof MapTabsMapImport - } - } + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/Welcome': { + id: '/Welcome' + path: '/Welcome' + fullPath: '/Welcome' + preLoaderRoute: typeof WelcomeImport + parentRoute: typeof rootRoute + } + '/(MapTabs)': { + id: '/(MapTabs)' + path: '/' + fullPath: '/' + preLoaderRoute: typeof MapTabsImport + parentRoute: typeof rootRoute + } + '/(MapTabs)/_Map': { + id: '/(MapTabs)/_Map' + path: '/' + fullPath: '/' + preLoaderRoute: typeof MapTabsMapImport + parentRoute: typeof MapTabsRoute + } + '/Onboarding/DataPrivacy': { + id: '/Onboarding/DataPrivacy' + path: '/Onboarding/DataPrivacy' + fullPath: '/Onboarding/DataPrivacy' + preLoaderRoute: typeof OnboardingDataPrivacyImport + parentRoute: typeof rootRoute + } + '/Onboarding/NextStep': { + id: '/Onboarding/NextStep' + path: '/Onboarding/NextStep' + fullPath: '/Onboarding/NextStep' + preLoaderRoute: typeof OnboardingNextStepImport + parentRoute: typeof rootRoute + } + '/Onboarding/PrivacyPolicyScreen': { + id: '/Onboarding/PrivacyPolicyScreen' + path: '/Onboarding/PrivacyPolicyScreen' + fullPath: '/Onboarding/PrivacyPolicyScreen' + preLoaderRoute: typeof OnboardingPrivacyPolicyScreenImport + parentRoute: typeof rootRoute + } + '/Onboarding/': { + id: '/Onboarding/' + path: '/Onboarding' + fullPath: '/Onboarding' + preLoaderRoute: typeof OnboardingIndexImport + parentRoute: typeof rootRoute + } + '/(MapTabs)/_Map/tab1': { + id: '/(MapTabs)/_Map/tab1' + path: '/tab1' + fullPath: '/tab1' + preLoaderRoute: typeof MapTabsMapTab1Import + parentRoute: typeof MapTabsMapImport + } + '/(MapTabs)/_Map/tab2': { + id: '/(MapTabs)/_Map/tab2' + path: '/tab2' + fullPath: '/tab2' + preLoaderRoute: typeof MapTabsMapTab2Import + parentRoute: typeof MapTabsMapImport + } + } } // Create and export the route tree interface MapTabsMapRouteChildren { - MapTabsMapTab1Route: typeof MapTabsMapTab1Route - MapTabsMapTab2Route: typeof MapTabsMapTab2Route + MapTabsMapTab1Route: typeof MapTabsMapTab1Route + MapTabsMapTab2Route: typeof MapTabsMapTab2Route } const MapTabsMapRouteChildren: MapTabsMapRouteChildren = { - MapTabsMapTab1Route: MapTabsMapTab1Route, - MapTabsMapTab2Route: MapTabsMapTab2Route, + MapTabsMapTab1Route: MapTabsMapTab1Route, + MapTabsMapTab2Route: MapTabsMapTab2Route, } const MapTabsMapRouteWithChildren = MapTabsMapRoute._addFileChildren( - MapTabsMapRouteChildren, + MapTabsMapRouteChildren, ) interface MapTabsRouteChildren { - MapTabsMapRoute: typeof MapTabsMapRouteWithChildren + MapTabsMapRoute: typeof MapTabsMapRouteWithChildren } const MapTabsRouteChildren: MapTabsRouteChildren = { - MapTabsMapRoute: MapTabsMapRouteWithChildren, + MapTabsMapRoute: MapTabsMapRouteWithChildren, } const MapTabsRouteWithChildren = - MapTabsRoute._addFileChildren(MapTabsRouteChildren) + MapTabsRoute._addFileChildren(MapTabsRouteChildren) export interface FileRoutesByFullPath { - '/': typeof MapTabsMapRouteWithChildren - '/Welcome': typeof WelcomeRoute - '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute - '/Onboarding/NextStep': typeof OnboardingNextStepRoute - '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute - '/Onboarding': typeof OnboardingIndexRoute - '/tab1': typeof MapTabsMapTab1Route - '/tab2': typeof MapTabsMapTab2Route + '/': typeof MapTabsMapRouteWithChildren + '/Welcome': typeof WelcomeRoute + '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute + '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute + '/Onboarding': typeof OnboardingIndexRoute + '/tab1': typeof MapTabsMapTab1Route + '/tab2': typeof MapTabsMapTab2Route } export interface FileRoutesByTo { - '/': typeof MapTabsMapRouteWithChildren - '/Welcome': typeof WelcomeRoute - '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute - '/Onboarding/NextStep': typeof OnboardingNextStepRoute - '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute - '/Onboarding': typeof OnboardingIndexRoute - '/tab1': typeof MapTabsMapTab1Route - '/tab2': typeof MapTabsMapTab2Route + '/': typeof MapTabsMapRouteWithChildren + '/Welcome': typeof WelcomeRoute + '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute + '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute + '/Onboarding': typeof OnboardingIndexRoute + '/tab1': typeof MapTabsMapTab1Route + '/tab2': typeof MapTabsMapTab2Route } export interface FileRoutesById { - __root__: typeof rootRoute - '/': typeof IndexRoute - '/Welcome': typeof WelcomeRoute - '/(MapTabs)': typeof MapTabsRouteWithChildren - '/(MapTabs)/_Map': typeof MapTabsMapRouteWithChildren - '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute - '/Onboarding/NextStep': typeof OnboardingNextStepRoute - '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute - '/Onboarding/': typeof OnboardingIndexRoute - '/(MapTabs)/_Map/tab1': typeof MapTabsMapTab1Route - '/(MapTabs)/_Map/tab2': typeof MapTabsMapTab2Route + __root__: typeof rootRoute + '/': typeof IndexRoute + '/Welcome': typeof WelcomeRoute + '/(MapTabs)': typeof MapTabsRouteWithChildren + '/(MapTabs)/_Map': typeof MapTabsMapRouteWithChildren + '/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute + '/Onboarding/NextStep': typeof OnboardingNextStepRoute + '/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute + '/Onboarding/': typeof OnboardingIndexRoute + '/(MapTabs)/_Map/tab1': typeof MapTabsMapTab1Route + '/(MapTabs)/_Map/tab2': typeof MapTabsMapTab2Route } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/Welcome' - | '/Onboarding/DataPrivacy' - | '/Onboarding/NextStep' - | '/Onboarding/PrivacyPolicyScreen' - | '/Onboarding' - | '/tab1' - | '/tab2' - fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '/Welcome' - | '/Onboarding/DataPrivacy' - | '/Onboarding/NextStep' - | '/Onboarding/PrivacyPolicyScreen' - | '/Onboarding' - | '/tab1' - | '/tab2' - id: - | '__root__' - | '/' - | '/Welcome' - | '/(MapTabs)' - | '/(MapTabs)/_Map' - | '/Onboarding/DataPrivacy' - | '/Onboarding/NextStep' - | '/Onboarding/PrivacyPolicyScreen' - | '/Onboarding/' - | '/(MapTabs)/_Map/tab1' - | '/(MapTabs)/_Map/tab2' - fileRoutesById: FileRoutesById + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/Welcome' + | '/Onboarding/DataPrivacy' + | '/Onboarding/NextStep' + | '/Onboarding/PrivacyPolicyScreen' + | '/Onboarding' + | '/tab1' + | '/tab2' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/Welcome' + | '/Onboarding/DataPrivacy' + | '/Onboarding/NextStep' + | '/Onboarding/PrivacyPolicyScreen' + | '/Onboarding' + | '/tab1' + | '/tab2' + id: + | '__root__' + | '/' + | '/Welcome' + | '/(MapTabs)' + | '/(MapTabs)/_Map' + | '/Onboarding/DataPrivacy' + | '/Onboarding/NextStep' + | '/Onboarding/PrivacyPolicyScreen' + | '/Onboarding/' + | '/(MapTabs)/_Map/tab1' + | '/(MapTabs)/_Map/tab2' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - WelcomeRoute: typeof WelcomeRoute - MapTabsRoute: typeof MapTabsRouteWithChildren - OnboardingDataPrivacyRoute: typeof OnboardingDataPrivacyRoute - OnboardingNextStepRoute: typeof OnboardingNextStepRoute - OnboardingPrivacyPolicyScreenRoute: typeof OnboardingPrivacyPolicyScreenRoute - OnboardingIndexRoute: typeof OnboardingIndexRoute + IndexRoute: typeof IndexRoute + WelcomeRoute: typeof WelcomeRoute + MapTabsRoute: typeof MapTabsRouteWithChildren + OnboardingDataPrivacyRoute: typeof OnboardingDataPrivacyRoute + OnboardingNextStepRoute: typeof OnboardingNextStepRoute + OnboardingPrivacyPolicyScreenRoute: typeof OnboardingPrivacyPolicyScreenRoute + OnboardingIndexRoute: typeof OnboardingIndexRoute } const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - WelcomeRoute: WelcomeRoute, - MapTabsRoute: MapTabsRouteWithChildren, - OnboardingDataPrivacyRoute: OnboardingDataPrivacyRoute, - OnboardingNextStepRoute: OnboardingNextStepRoute, - OnboardingPrivacyPolicyScreenRoute: OnboardingPrivacyPolicyScreenRoute, - OnboardingIndexRoute: OnboardingIndexRoute, + IndexRoute: IndexRoute, + WelcomeRoute: WelcomeRoute, + MapTabsRoute: MapTabsRouteWithChildren, + OnboardingDataPrivacyRoute: OnboardingDataPrivacyRoute, + OnboardingNextStepRoute: OnboardingNextStepRoute, + OnboardingPrivacyPolicyScreenRoute: OnboardingPrivacyPolicyScreenRoute, + OnboardingIndexRoute: OnboardingIndexRoute, } export const routeTree = rootRoute - ._addFileChildren(rootRouteChildren) - ._addFileTypes() + ._addFileChildren(rootRouteChildren) + ._addFileTypes() /* ROUTE_MANIFEST_START { diff --git a/src/renderer/src/routes/Onboarding/NextStep.tsx b/src/renderer/src/routes/Onboarding/NextStep.tsx index 581b16e..0280473 100644 --- a/src/renderer/src/routes/Onboarding/NextStep.tsx +++ b/src/renderer/src/routes/Onboarding/NextStep.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/Onboarding/NextStep')({ From 819788507bcd5950c3b3ac26e0c84b405271eea1 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Tue, 3 Dec 2024 10:37:53 -0500 Subject: [PATCH 6/7] Adds aria label. Removes nested ternary. Adds todo. --- .../src/components/OnboardingTopMenu.tsx | 31 +++++++++---------- .../MetricsDiagnosticsPermissionToggle.tsx | 1 + .../PrivacyPolicy/PointContainer.tsx | 2 +- .../src/routes/Onboarding/DataPrivacy.tsx | 6 ---- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/components/OnboardingTopMenu.tsx b/src/renderer/src/components/OnboardingTopMenu.tsx index 6d51320..4515f8e 100644 --- a/src/renderer/src/components/OnboardingTopMenu.tsx +++ b/src/renderer/src/components/OnboardingTopMenu.tsx @@ -14,7 +14,6 @@ const MenuContainer = styled('div')({ margin: '16px auto', position: 'relative', }) - const GoBackButton = styled(Button)({ display: 'flex', alignItems: 'center', @@ -29,18 +28,19 @@ const GoBackButton = styled(Button)({ backgroundColor: 'rgba(0, 0, 0, 0.1)', }, }) - const BackArrow = styled('span')({ fontSize: 24, color: WHITE, }) - +const StepsContainer = styled('div')({ + display: 'flex', + alignItems: 'center', +}) const Steps = styled('div')({ display: 'flex', alignItems: 'center', gap: 16, }) - const Step = styled('div')(({ active }: { active: boolean }) => ({ backgroundColor: active ? WHITE : 'transparent', color: active ? BLACK : BLUE_GREY, @@ -49,7 +49,6 @@ const Step = styled('div')(({ active }: { active: boolean }) => ({ fontWeight: active ? 'bold' : 'normal', whiteSpace: 'nowrap', })) - const Divider = styled('div')({ width: 16, height: 1, @@ -77,6 +76,11 @@ export function OnboardingTopMenu({ currentStep }: OnboardingTopMenuProps) { const navigate = useNavigate() const router = useRouter() const { formatMessage } = useIntl() + const stepToRoute: Record = { + 1: 'DataPrivacy', + 2: 'NextStep', + 3: 'PrivacyPolicyScreen', + } return ( @@ -88,25 +92,18 @@ export function OnboardingTopMenu({ currentStep }: OnboardingTopMenuProps) { gap: 8, padding: '12px 32px', }} + aria-label={formatMessage(m.goBack)} > - + {formatMessage(m.goBack)} {[1, 2, 3].map((step) => ( -
+ - navigate({ - to: `/Onboarding/${ - step === 1 - ? 'DataPrivacy' - : step === 2 - ? 'NextStep' - : 'PrivacyPolicyScreen' - }`, - }) + navigate({ to: `/Onboarding/${stepToRoute[step]}` }) } > @@ -114,7 +111,7 @@ export function OnboardingTopMenu({ currentStep }: OnboardingTopMenuProps) { {step < 3 && } -
+ ))}
diff --git a/src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx b/src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx index cf9bf7e..6320a00 100644 --- a/src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx +++ b/src/renderer/src/components/PrivacyPolicy/MetricsDiagnosticsPermissionToggle.tsx @@ -33,6 +33,7 @@ export const MetricsDiagnosticsPermissionToggle = () => { const togglePermission = () => { const newValue = !isEnabled setIsEnabled(newValue) + // TODO replace code once the Metrics and Diagnostics handling code is incorporated localStorage.setItem('MetricDiagnosticsPermission', String(newValue)) console.log('Permission updated:', newValue) diff --git a/src/renderer/src/components/PrivacyPolicy/PointContainer.tsx b/src/renderer/src/components/PrivacyPolicy/PointContainer.tsx index 6adcc9e..c10eb27 100644 --- a/src/renderer/src/components/PrivacyPolicy/PointContainer.tsx +++ b/src/renderer/src/components/PrivacyPolicy/PointContainer.tsx @@ -38,7 +38,7 @@ export function PointContainer({ {isSvg && icon ? ( React.createElement(icon, { width: 18, height: 18 }) ) : ( - + )} {title} diff --git a/src/renderer/src/routes/Onboarding/DataPrivacy.tsx b/src/renderer/src/routes/Onboarding/DataPrivacy.tsx index 6faad02..1139a37 100644 --- a/src/renderer/src/routes/Onboarding/DataPrivacy.tsx +++ b/src/renderer/src/routes/Onboarding/DataPrivacy.tsx @@ -63,7 +63,6 @@ const Container = styled('div')({ height: '100%', backgroundColor: DARK_COMAPEO_BLUE, }) - const ContentBox = styled('div')({ backgroundColor: 'rgba(255, 255, 255, 0.94)', border: `1px solid ${BLUE_GREY}`, @@ -79,7 +78,6 @@ const BodyTextWrapper = styled('div')({ margin: '16px auto 0', textAlign: 'center', }) - const ButtonContainer = styled('div')({ display: 'flex', justifyContent: 'space-between', @@ -87,13 +85,11 @@ const ButtonContainer = styled('div')({ marginTop: 63, padding: '0 20px', }) - const StyledIcon = styled(LockedIcon)({ marginBottom: '16px', width: 40, height: 50, }) - const BulletList = styled('ul')({ width: '50%', textAlign: 'left', @@ -101,11 +97,9 @@ const BulletList = styled('ul')({ color: DARK_GREY, paddingLeft: 0, }) - const BulletListItem = styled('li')({ marginBottom: 8, }) - export function DataPrivacyComponent() { const navigate = useNavigate() const { formatMessage } = useIntl() From 787355a66d53b13e3ce10eb0cf31440770d2b667 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Tue, 3 Dec 2024 10:49:16 -0500 Subject: [PATCH 7/7] New red dot. --- .../src/components/PrivacyPolicy/index.tsx | 2 +- src/renderer/src/images/RedDot.svg | 1 - src/renderer/src/images/red_dot.png | Bin 0 -> 8928 bytes 3 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/renderer/src/images/RedDot.svg create mode 100644 src/renderer/src/images/red_dot.png diff --git a/src/renderer/src/components/PrivacyPolicy/index.tsx b/src/renderer/src/components/PrivacyPolicy/index.tsx index ceb9d35..cde0c04 100644 --- a/src/renderer/src/components/PrivacyPolicy/index.tsx +++ b/src/renderer/src/components/PrivacyPolicy/index.tsx @@ -5,13 +5,13 @@ import { useIntl } from 'react-intl' import { BLUE_GREY, DARK_GREY, VERY_LIGHT_GREY, WHITE } from '../../colors' import BarChart from '../../images/BarChart.svg' import BustInSilhouette from '../../images/BustInSilhouette.svg' -import RedDot from '../../images/RedDot.svg' import Wrench from '../../images/Wrench.svg' import ChevronUp from '../../images/chevrondown-expanded.svg' import ChevronDown from '../../images/chevrondown.svg' import ClosedLockWithKey from '../../images/closed_lock_with_key.png' import RaisedFistMediumSkinTone from '../../images/raised_fist_medium_skin_tone.png' import RaisedHandMediumSkinTone from '../../images/raised_hand_medium_skin_tone.png' +import RedDot from '../../images/red_dot.png' import { Text } from '../Text' import { DiagnosticItem } from './DiagnosticItem' import { MetricsDiagnosticsPermissionToggle } from './MetricsDiagnosticsPermissionToggle' diff --git a/src/renderer/src/images/RedDot.svg b/src/renderer/src/images/RedDot.svg deleted file mode 100644 index bb5da97..0000000 --- a/src/renderer/src/images/RedDot.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/renderer/src/images/red_dot.png b/src/renderer/src/images/red_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..68b4618f64bcc563b8322dd81172409339433b9f GIT binary patch literal 8928 zcmV<6A|Ks}P)005u}1^@s6i_d2*001JRNklO?UArRQ zsKTfR?Q((L0y_nE2%KXe5kN#JfHMJf;z1J+R0(1Mp8vT9kI8?mkY;es&&%yl)w<&xD_!e-a1#q#w z2@uVd?o|S>jNo!3k{ItwqX0M)z-9t>8!BJuhD#;%P|gHlCHC{~S?lgMP(l~nFQ9Y* zkhZv=UrR0gP~l4oUzA)Km%Ia8(lMaIS-^D$-eBNmMr0!LDp!CO6#y33h5%ToULC@w zBt#nAFG0LlPeKOZ3t(#kuuwtk6*0W-??2_>>kd8%d=)s}D&Xn04FbPv;CcgBMlQ+&`Q`;bGIC*VF9K8 zxpQ#4gPRrp#+P|o02k2C6Zj1SZ;IgZsDMkANQBtC+TwcYf(sz)Dp*T8ObDGSq6!V? zdfBB?mJvZ4&INpR*Q?&$Ho<2Db+oEEgd$}Ab^)e;><=A$T;bEeqb-1^KC!qyg11B> zmlr_A0bFqnv=F$cP`yGCYp@wr#9G8!aIT*X8v}8SbUK(ML6QWL1c_saqG}z`S~SZb zP0=*Pr77C$L6+g1E3;yZL8AzbqCD~N5ZVAp>w&ZlKo;k0$Ze~7mxYf3H!p>RWw8Tj zfY%v#TP*V82y)N@;9{?K0{{+5J|aMz%Yn>WYJ8#MVG8pv!{$S@5__^_vbp1bohOyg2KCix|Eu47@9XUynsh37&^7 z1Tn5b7X{Eo#^lghi=-*WIZPZAjg1kVdoJ;L=Min%M6}^t%)|ud?A6G~2*fdng9yhRZS{W6&hQ zd4;rri?$+t5DV!1V~X2_dyYh2oT*-A;j<3j4}7mxz`13%YXxqI;D%UaLmWUBlMlca z_e}_6&HV~sti^cfqR~;}bIu{&wvBk(vx&ECCR(>1IcpWfQT}0Ai&~2~*BI-`+}}fU zY-kMP-7k>4?c*n~hn^&RWIx%1KP7wcA+je9VkakY#^5}RI>ZfUUMVvVVH&7mc~*Vg z!i@?y0*4nBfd69=0VsJXnm^Bl54Cfq@WjSymY(c$m0seDUevgXeK*n(%8)g~q$Pu!KA12+qk8IyQ(#Iad zO;6+E1ee6Q8V2sR?t+VR+H@aoQ`P?pe7#k`N(oKJjL7drBEOfwcoZt0hcBu_7{-Dk zxohq9Fsj6B*OF}CPG{#u#OI%nnV3L~fh>j0CmKT(O%`6^8?Pzkab+AqoS@F(o_dPx zCqJQg?|r2E_K_VvoCDbD;EX9HhsV>u_BUVyJ*M)hhr@1Eoy!pM6 znN`3u2%x|ljmRG+BF`@4JCO=>m2=>7jVjUfG$xAZY}-cX7k1IPXeZISwYi&{?t!xv zpu!Q1Vw!-QY4tD9GBBc2{@g^+?SKgO&|!M_-AnHue?afS2XWS7yWKot36w6ax#AMW zzB?vW{i##^PrxV67y}Znj%IoqBSL4}Ho7mk zg3ivJn6a_k#rD!XrfZ1jB>2kDG`9 zcgca`^#TPfAWW-}rWyRPQ++RIU<%mjcflfC4g5(AzuEEbGzna79l#{4@uexoT9Qqh z7`gIFI+tCFSu>9GdXT1|{nDHa{9FC1WsxDM^^#5n%3z$im~@yVXs3faa+Ka(KVs^R zJ4g>4z(o-@Nhv+Sm0sl{-tQ)yh8PHqQBosAn9NaKEd>NzsvOZ?<75X z6xSWWMkaT;GePKY_i`A&KuSW!Qo_hf! z*IYw<{9rT60pauw`!1`?BA z7#ph63$CTA4_MW=l23+R9xhl!UK7EG6Oqk{SH6xh3{$~agxEQvscGVMYZ-a*Rdjd% z0%l|crl#_YR{C0Zx-;3u0AYw=IsecY7#YD$O)-7vKQi^rZ;>53j2-EcnZ6n}sb9~z z{CfVg{T6-)_`-5fz<&|Ljj_o3Lg5PKEAA&iVoYEC(v-+rx)(l|kzaf%@ju&|$9Y*+ zODg1y)bbV!fc+YxRv#0`Xs1K=vxk}b`)@FP|BtazM3yA|j5q+USBEjrmY>Yv2H-Ob z`JSR*8y#K9*MCC{A4x>k6)v|cLf7B!7?Zo=LJdWTr>04|9Y(L&!{~L_60Mzp6DN=? zE3?~Ts#Ilq)CxTYri?iUHlGw>)=m)b*p5jObpHXEnnseiI@eL8oc+_!-~{sJr!RN# zGvMBZ0%Rd4BfLHqxiL|lNPun;_XY7@9Gnw{Xljz=+;bUy`7hFW-i5j19shaZ#&cy; zM{7fqyuSWB2giSoNfJh1vWMv0bC|s4?@13H#B@gx>CcHY#`OPl`~TqnL!SU{%v9|{ zDd9o^_$#rn{eEv2>!9DT_;uI;ShlSwPAXHfG;3|b% zfrE?A1uJZd;A08w2umwXZnq;slx9ScFnaaXbg$T*$1IbRjc>m7z9$zC);l$bDmpod zBncx|Uj^MRled2h(hQO~2exp*;cd!=R6A1msD)oA?|bvi0n}6TW=9QtFor9OjJ6ZF z+JTraO^M=|ReP?cv+L5_J!CdWG_)mX!IBhUdK!%*y3gN*L=h)$yFFh@kR%ldDb}$R z5R0JBU8(Rvg*S5w#*Sk-HE-+>Vv*O^i^+{e<}B;OciF`-J)KXN7pWAwWS06@Yx7sb z%4gflXMcAtyBL5Ix80sEs*0;h=sbj#wj!#p&s6sU|M83isPLN+yt`8?Fc&qyPB^TV zWyHp?>gpHLx%hc`yjNzrEoP@1N@G)8X7lgDSx$WO+xgtd0K#%MPT-pT1lo6Jd42Fx zXUqjFToS{dCL*0;@<4rft(;U~Eio{9#q&vaKCe{1VG&-7<7ooXSyUa#&gU^YoicgH zcTsEeReE*xgZ>(3k&deVX$Jonxcf{1cyYufw7S))}u1m2ePN4TcfOa}1e9kYT za=u+i+0RDgTBrIB;J=)<7*JKOiACNSuGuJ#I5;A}^m=r*Zl=5Y^1Pnc>s2~UwaRzq zm9UZ>CkZ3FFGr6b$348CsN2Q)@3VleY%9`lDe}%#^&iMJU z23-hY1Vqz4k~QOu?B0cR5@c#>pqpqL?=3Dhl&LA`cIfWjg?;)sdh{5YBm*73!ag=m zWVKU$uY+GE?~lE33MhG-j2D%0UTtD&KjXC|orIBHmlCZRLnbHl=~L3`-J-4&s>0+X z(V8(vc3q0zeg|Y0jiY`eB@d$Yp_jk@qEz*5z#lFcfDU%WB5x0uMwWB#WFooa!6M=enNKEci7sytZr*6m%#E7m7z&Mtt_!bT7I96B+PH zM=@`=&32atgaWATVLA!ji!Q((If|Y*fyObhSG@wnuxV1P`r0162Kel$15jklz`J6I zE7@+%^%_+SLTCH=MB`)m4BVEurJX5HO%sie(b;}J_DA=X4L}Q5Tqt0Wn8caLyBypy zmxLsqV_f#e7_N+icDMB&EfHX{jAYYBlC7J4X98Ca=d^BjiMnG|m{yXlo5>D7fjjUR zQIeDpmI_#anXBNHJ-8nD$f;1kb0fH+-jA=|mrtxC8X2K;{x(DeP1EY^+7`g2lL^zH=vcpHvUP=_1ygpbo&!#fG z_NIMG-;Q!D=@>A)-Se(^>n7|!{gep7`HDvzHqP`AW~vvtTm?UP(q$M2=S1+YhbA0F zgYV1{kF6%zv@y?kGdtjVTU!RM7)Um4Bs=&d?$~jnI4a9-ao~z=Um4$$<&BS?IH@v> z!0RH|S;l#_2H+8t?QoOL8!;o@yzEl6wQb$*^0Hov8R?R2-bniN|3ix%381oDL6paV zWN?v%*8zVqZv|91+rXO(@G5=B;)!vh_3O|y?f2muT3>2ysl|i+?@=LDqV?;D z*F1rH>S#p;i+ZpXA@iIV)i>K*1sgZh89E(H;MxeTC_B?rEj%0YH3r5Qk`3!IQG}+} zC)I7uZ#lW&N?#glF>yq)VLjQ=r*Wa``B*T7F@q}{Tnqg5d=xM;aD6Nig{7BpqnDyP zX*4!QG(L`IDL8FozU5vQ1T=F*aB)8aGq##&)hO=x38Khg zi>=b=1L>7;O{V(VMgSW?88`6ida@DjHd*Y&5{<7$;t0~Lzml(2zm?&NtwZ97XnZx< z&rkGs9i_HEUtbAd?cf8{hUW$qQ1wMdP6O&iJ7P^(Ei_z+{ zFiDbc2o#EyYM-6_xWuWx2>4PJz`@;sUJ;4JP1~E6duym-&Kk`(@5{1E%h=XdkU57W z3DH@r$eubvxGR0xd8@C6u~mJ=0Dz$jwy-W1d1)o?3(h5w#t@yo3QLt<3U_ zmwrTgfVQfByCb-;DgG<>(^bWEyF?>hzs;2^2d#NOEJ=*Sab?|ic?2*D zT&%DizZXe360*yPtTjWFuk6*=N%ESXwdKBMEqp6ID9rb0kxl}=UKu9};Zlu~skNx? zDgo@jhU~7#eBu3)lQ3 zA|?Vc`7liDdRwbNs30b1#PdOzPvqbg@!$@tx)sAVuXzB7>4|hSGM? zZLN}kO1|f@M8+>k7sT&^+>nTzDBNLqGu$gE+r!Li@S57KCV;vz9aBdp`j|w6(P_W6kacbP(b|k!Sh(OYgDBaeoKdw!MaE)OHwbJjH>vYWDvQje^4iXo4GLQq z+}c1KSog_PfXjc>hj0U`8)9ICh^!V>#F$FmkAPuC%H*Rh!L&9Lgau+!Yd~LCSn{no zR-?KhR@flQ!sQ)o&P>mAM3dv`th+U~3F)mLO9Z-YG%LwI8juHZzPm9L6HY$S-Y*fpv3x)pq7f zYjaFQhP&Omt&4$ihGn(kPKl~{BGP`&t<3>IokP3`jK>12WJuys5C4R=0NvW`abTn1 z)s)be`Buf6Pm39v(Nr4Kv}3@n%|!tl?o0FIyT(8gPCS@vy4_l9C&Na_nt@L2!00?k zZZ)vAxu~Gdz*BL}fr*Q>xZIq0$cq(=j#~g*n+-rW1jLGf`QhGT6T}&VN@jS$)>@m* zd(L3a9|wk8 zv)2!4U2tpjQN55Ozl@_^s8-cUgKro(6I}D~>8-W!&B#fOvm>#>A;nLpE>tkslsKG# z+QQe`ToRCaChR<4s#^E7hhkvA`io&2%?!y%3u0^YO+H-xdL9QJFba>T4-0jca!VTn zwl)_9oJ}45p$ZSjz~lLboQ%{J&&>hK)>@kZvO(qRAJf3&#sm2DunKkyUTdcc#u{T! z1CJL9_?W7mqkcGUnEDmTh-p9H)*5V>$8`0d_;b`I zA+5~=#ESb>tzUlxI2Hq`gNGVZ=U|dTSdeZZY^`q6;Ck_5<{DhEgNJ~$=neB@H-q19 zmoSSgIWOSpZKs--D^e8b4{@zZ7s01`IlYl96567 zbEz!Oz_|zI5=$mgI(=Kids&a4YxK7z5zx&JEi;0**dJMKPcu1#^qXH zk$q0}9w%~*3m`0>C_%la9SLb|sOweDpX8LYh{{ zwbt592}nHy_UG>u@gG1e)&1UXv+(;C;v!=yHk)e~E44NgfK4?&TV%cc+nK_x;d4!e zT#-AS>ixwNN*^8uk!>8<+K~9q1u8~AqpHR6WXJ!;|qSlDH~`dO+wCl!aQ zom*SEnQxQwRa6lE7QX~|HULrhyj6X>g*9P0#+CTxrVY;?mM;rT9V!t5k4aDjuf#btoFP52q+2=CVe;dJ0TiZw2(%*#6NSgz% zL;!1LmI`=Ql}zflU;7*Szyx&5W1%BSvW=_wxrnt4Gr+SW=hup%@%*TD1 z!uth|HWi-hhndTRyo>!p2A>ZC%zCXgg3|?YH)|vjs!~K1R{o-{Pvf*`WOFrv$Lt?)ycfW_6>U)87uD>IC)yT+v{-#G2 zx&kj1krF(q@3KC}edk(R!ih&s`O-Z0d!N9k=jv7G<${Ct!yn4v+ga^@z_2*5b;HX; z{RUicUH#zO3jb{`IYoGw`91+S>Qvv8sXpyxw<5=(Ot+o2mVVqflk3g;uJ>uD`Uk*~ z`ToB7y5PbdQjv~`yu^4HOwAS3y5c1Z;EeDENoeMSy$t?HL~bg>vUwb_oa8vJ!k=ZT zR~eCO%S6PV`w!A^Z`gs}GOaCM_y*?p4OhRfDg4>V{?6!CqoXJL+@8XZ6kd(USeT4d zlrOm5g10mPbi>Ln0eoqmcs%0ZU*S7los2!x(X+cPygO6v1$Ce@Fs&?VqPcf)Yl{QC zCg_UqE~_RRh4S?*yqkQn(Mj7gQNY6Pbufa+o>2WHs0UKJ-?n#Ke5yB->vdtt#j9Ti z?*l&cOn?8W01)6i3Of+F$an}D0%4OoZe8=@fKP|SeOWN{n&$WaH}DS6Pg+XG=PK+;@kAw*!->;%}ID9dL&F-~ol-N>%r!)huaX zrtGj9Ve67l4|Kz7eYW;ph2NovZ?D2z1rE%20Att@pJEld*TP#+{S+dbw5EL8D&dlF zyDkupv^Z1`Sa=I?&#C--qF+8WF<@vvBkzEI9k7ZZCLFl!8Kfmf7TXsA+@ zE;md#R1Z0Li@+_XO8JT-`jyd9PW6idd!6cos(LLjQcQXsRu8Q2!qnQr0_Yl)FRb;| z-0l$z{|5N$h4{PrEqYJAea1p0aHGIPIZD#tj!m!~SOI7Ye<$B61_JKAhGQh12(ILwMn^UF(uhjmr&pG71|=+M4nOW4uphstXDq zocv^j`Ph999#D}zz}QUjAnp3%1y43+EVcAq?|!TL+sv}2cPS`fVPCMSM-cg_s$NiR zhJ#brnLr~7@J2>|i${~K21r@$^?O`(7~2MC*7v#vn+>KSf*%d9$l zSq;7n9?al(6+VbaiU_Csi1o~7p8w0L-m4<-bE?;=h~L0CPfo&3a!j#ZSbZd=b-#_x z!Yhl%=`SD;#^=A{RR6ueU1##|k6tx0a>_?S<~eHen8FuSr03vr&F2E`QMzGC#;`__b(fpwF88Q~KUURu01ut%_jsmUaNVYXe{dq-SJRg;VDJN2rer?lp2Wjot(=1L6}j8aJIE6C%b@3=9ow{sU87z zGqp%7z|*P%-lgy{r+PCd(W#(Sz?tnlM1CWJH=6*&QU!-7;((iPP)8O>9cPQ{DvxPs zb=g)TJ!3)j3s;8zV?Z{X@k#DvL70pm;{R=^8@)KlQ9 zXL7AlbE{2>Mk9E&`ktS^=N#Pb;AYLY!g_qkeU(eX1rK*~X9K@v;0*#VGcZwaA#AAj ztT7D`4qYnR0Oni)lm=y+HM4D~?G5ZilwUidaEpUa0$*i@eK;2VcX;Cx2C&A}3%a zjThv5`S1z>u)$U-YzB4+YzHn7c$UBxV53iPMrBBoO_^)vn`gR{nkNd6DLkV2B!CAr zpZvL3U_WqTC4LFgw!`S#hbJaFz!-2Yu+g8l2yF81e4Ko-{fIz9zrCH)yn(=!2k9vA uq{0E<5%L{_9`om8K(GC@bNl}t;J0Wk#6|G{0000UqIae literal 0 HcmV?d00001