Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: onboarding data privacy #48

Merged
merged 7 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/data
/messages
/translations
src/renderer/src/routeTree.gen.ts
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
Expand Down
144 changes: 144 additions & 0 deletions messages/renderer/en.json
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't review any of the translation strings in this file or others. Let me know if that's wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is fine!

Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
{
"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."
},
"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"
},
Expand All @@ -16,5 +46,119 @@
},
"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"
},
"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.shareDiagnostics": {
"message": "Share Diagnostic Information"
},
"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."
}
}
11 changes: 7 additions & 4 deletions src/renderer/src/Theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
},
},
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/src/colors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
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 DARK_GREY = '#757575'
export const VERY_LIGHT_GREY = '#ededed'

export const ORANGE = '#FF9933'
export const DARK_ORANGE = '#E86826'
Expand Down
122 changes: 122 additions & 0 deletions src/renderer/src/components/OnboardingTopMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { styled } from '@mui/material/styles'
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'
import { Text } from './Text'

const MenuContainer = styled('div')({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '55%',
margin: '16px auto',
position: 'relative',
})

const GoBackButton = styled(Button)({
display: 'flex',
alignItems: 'center',
gap: 8,
backgroundColor: 'transparent',
color: BLUE_GREY,
fontSize: 16,
padding: '12px 32px',
borderRadius: 20,
whiteSpace: 'nowrap',
'&: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 ? WHITE : 'transparent',
color: active ? BLACK : BLUE_GREY,
padding: '12px 32px',
borderRadius: 20,
fontWeight: active ? 'bold' : 'normal',
whiteSpace: 'nowrap',
}))

const Divider = styled('div')({
width: 16,
height: 1,
backgroundColor: COMAPEO_BLUE,
alignSelf: 'center',
margin: '0 12px',
})

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 (
<MenuContainer>
<GoBackButton
onClick={() => router.history.back()}
variant="text"
style={{
color: BLUE_GREY,
gap: 8,
padding: '12px 32px',
}}
>
<BackArrow>←</BackArrow>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to give this an aria-label for screen readers. I don't think screen readers will do a great job with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok done

{formatMessage(m.goBack)}
</GoBackButton>
<Steps>
{[1, 2, 3].map((step) => (
<div key={step} style={{ display: 'flex', alignItems: 'center' }}>
<Step
active={currentStep === step}
onClick={() =>
navigate({
to: `/Onboarding/${
step === 1
? 'DataPrivacy'
: step === 2
? 'NextStep'
: 'PrivacyPolicyScreen'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: might just be me, but I find nested ternary expressions hard to read. Maybe something like this, if you want to maintain this being a single expression?

Suggested change
step === 1
? 'DataPrivacy'
: step === 2
? 'NextStep'
: 'PrivacyPolicyScreen'
{
1: 'DataPrivacy',
2: 'NextStep',
3: 'PrivacyPolicyScreen',
}[step]

Copy link
Contributor Author

@cimigree cimigree Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I replaced it with an object but not exactly as you suggested because sorry I wasn't sure what you meant/ how to do that.

}`,
})
}
>
<Text kind="body" bold={currentStep === step}>
{formatMessage(m.step, { number: step })}
</Text>
</Step>
{step < 3 && <Divider />}
EvanHahn marked this conversation as resolved.
Show resolved Hide resolved
</div>
))}
</Steps>
</MenuContainer>
)
}
42 changes: 42 additions & 0 deletions src/renderer/src/components/PrivacyPolicy/DiagnosticItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import CircleIcon from '@mui/icons-material/Circle'
import { styled } from '@mui/material/styles'

import { DARK_GREY } from '../../colors'
import { Text } from '../../components/Text'

const DiagnosticsItem = styled('div')({
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
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 = ({
title,
description,
}: {
title: string
description: string
}) => (
<DiagnosticsItem>
<CircleIcon
fontSize="small"
sx={{ fontSize: 6, color: DARK_GREY, marginTop: 1 }}
/>
<DiagnosticsTitle kind="subtitle">
{title}:{' '}
<DiagnosticsDescription kind="body">{description}</DiagnosticsDescription>
</DiagnosticsTitle>
</DiagnosticsItem>
)
Loading
Loading