diff --git a/.env.gps b/.env.gps deleted file mode 100644 index 4c368b9cfb..0000000000 --- a/.env.gps +++ /dev/null @@ -1,15 +0,0 @@ -# This is the GPS debug/dev .env file. Any debug or non-release-signed build will -# use these flags. - -# About flags: -# ------------ -# Any variables that start with flag_ will be available to the component -# For now, only true/1 is parsed, everything else will be interpretted as false -# flags must begin with flag_ and are case sensitive - -TRACING_STRATEGY=gps -AUTHORITIES_YAML_ROUTE=https://raw.githubusercontent.com/Path-Check/trusted-authorities/master/staging/authorities.1.0.1.yaml - -flag_google_import=true -flag_custom_url=true -flag_download_locally=true \ No newline at end of file diff --git a/.env.gps.release b/.env.gps.release deleted file mode 100644 index 0ee640514e..0000000000 --- a/.env.gps.release +++ /dev/null @@ -1,8 +0,0 @@ -# This is the release .env file, it is built for App store final releases -# only prod ready feature flags should be enabled here. -# -# For Android, this file is automatically used for any release build. iOS should -# prepend ENVFILE=.env.gps.release before any build commands. - -TRACING_STRATEGY=gps -AUTHORITIES_YAML_ROUTE=https://raw.githubusercontent.com/Path-Check/trusted-authorities/master/production/authorities.1.0.1.yaml \ No newline at end of file diff --git a/.env.gps.staging b/.env.gps.staging deleted file mode 100644 index 42ee9060d1..0000000000 --- a/.env.gps.staging +++ /dev/null @@ -1,16 +0,0 @@ -# This is the Staging .env file, it is built for automated testing enviroments -# Flags that are ready for testing/staging should be enabled here. - -# When making any Staging channel build on both iOS or Android, prepend the build -# command with ENVFILE=.env.gps.staging -# -# e.g. -# -# ENVFILE=.env.gps.staging ./gradlew assembleRelease - -TRACING_STRATEGY=gps -AUTHORITIES_YAML_ROUTE=https://raw.githubusercontent.com/Path-Check/trusted-authorities/master/staging/authorities.1.0.1.yaml - -# flag_google_import=true -flag_custom_url=true -flag_download_locally=true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9a728ee657..fc3ca650a1 100644 --- a/.gitignore +++ b/.gitignore @@ -99,9 +99,7 @@ android/app/release safepaths.realm # env files -.env -.env.bt* - +.env.* # private resources pathcheck-mobile-resources diff --git a/README.md b/README.md index 81e60651bb..a8436fd48e 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Safe Paths is designed to support a range of DCT and public health use cases. Cu The Safe Paths app is being developed to support a variety of build 'flavors' of the application around core health and tracing functionality. Reach out to our team to discuss creating a flavor for your use-case. ### Path Check Release of COVID Safe Paths + Safe Paths is available as an app published by Path Check in the [Apple App Store](https://apps.apple.com/us/app/covid-safe-paths/id1508266966) and the [Google Play App Store](https://play.google.com/store/apps/details?id=org.pathcheck.covidsafepaths). Any authorized pubic health authority can use Safe Paths. ### Custom Builds @@ -80,43 +81,45 @@ If you're looking for a first ticket - please check out the backlog for a bug or View the [architecture diagram](docs/Private_Kit_Diagram.png) for a basic overview on the sequencing of generalized events and services that are used by Safe Paths. - ## Developer Setup First, run the appropriate setup script for your system. This will install relevant packages, walk through Android Studio configuration, etc. **Note:** You will still need to [configure an Android Virtual Device (AVD)](https://developer.android.com/studio/run/managing-avds#createavd) after running the script. -#### Linux/MacOS +### Linux/MacOS -``` +```Shell dev_setup.sh ``` -#### Windows +### Windows -``` +```Shell dev_setup.bat ``` -#### Environment +### Environment -Populate the following 2 `.env` files with the relevant urls for your GAEN server: +Populate the following `.env` files. View an example file at `example.env` +```Shell +.env.dev +.env.staging +.env.release ``` -.env.bt -.env.bt.release -``` -**Note:** Members of the `Path-Check` org can complete this step by running `yarn set-ha` and passing in the 2-letter ha abbreviation as the first argument (i.e. `yarn set-ha pc`) +You can configure `AUTHORITIES_YAML_ROUTE` against `https://raw.githubusercontent.com/Path-Check/trusted-authorities/master/staging/authorities.1.0.1.yaml`. + +`ZENDESK_URL` can be omitted in development, and the Report Issue page will throw an error when submitting. ## Running **Note:** In some cases, these procedures can lead to the error `Failed to load bundle - Could not connect to development server`. In these cases, kill all other react-native processes and try it again. -#### Android (Windows, Linux, macOS) +### Android (Windows, Linux, macOS) -``` +```Shell yarn run-android ## for the location enabled app ``` @@ -126,13 +129,13 @@ Device storage can be cleared by long-pressing on the app icon in the simulator, First, install the pod files: -``` +```Shell yarn install:pod ## only needs to be ran once ``` Then, run the application: -``` +```Shell yarn run-ios ## for the location enabled app ``` @@ -159,7 +162,7 @@ This project is using [typescript](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html). Run the complier with: -``` +```Shell yarn tsc ``` @@ -200,7 +203,7 @@ Tests are ran automatically through Github actions - PRs are not able to be merg To run the static analysis tools: -``` +```Shell yarn validate ``` @@ -208,13 +211,13 @@ yarn validate To run the unit tests: -``` +```Shell yarn test --watch ``` [Snapshot testing](https://jestjs.io/docs/en/snapshot-testing) is used as a quick way to verify that the UI has not changed. To update the snapshots: -``` +```Shell yarn update-snapshots ``` diff --git a/android/app/build.gradle b/android/app/build.gradle index aae0448de5..40139ea22e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,9 +5,9 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'realm-android' project.ext.envConfigFiles = [ - debug: ".env.gps", - staging: ".env.gps.staging", - release: ".env.gps.release", + debug: ".env.dev", + staging: ".env.staging", + release: ".env.release", ] apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" diff --git a/app/Entry.js b/app/Entry.js index 40a33fa8ef..14c938c195 100644 --- a/app/Entry.js +++ b/app/Entry.js @@ -33,6 +33,7 @@ import ShareDiagnosis from './views/onboarding/ShareDiagnosis'; import NotificationsPermissions from './views/onboarding/NotificationsPermissions'; import LocationsPermissions from './views/onboarding/LocationsPermissions'; import LanguageSelection from './views/LanguageSelection'; +import ReportIssueForm from './views/ReportIssueForm'; import { Screens, Stacks } from './navigation'; @@ -89,6 +90,7 @@ const MoreTabStack = () => ( + ); diff --git a/app/api/zendesk/reportIssue.tsx b/app/api/zendesk/reportIssue.tsx new file mode 100644 index 0000000000..79631e27e2 --- /dev/null +++ b/app/api/zendesk/reportIssue.tsx @@ -0,0 +1,93 @@ +import env from 'react-native-config'; +import { Platform } from 'react-native'; +import getAppVersion from '../../helpers/getAppVersion'; + +interface ReportIssueProps { + email: string; + name: string; + body: string; +} + +const OS_FIELD_KEY = '360033622032'; +const OS_VERSION_FIELD_KEY = '360033618552'; +const APP_VERSION_FIELD_KEY = '360033141172'; +const APP_NAME_FIELD_KEY = '360034051891'; +const ISSUE_SUBJECT = `Issue from GPS mobile application PathCheck ${ + __DEV__ ? '[Dev Testing]' : '' +}`; +const ANONYMOUS = 'Anonymous'; +const APP_NAME = 'PathCheck GPS'; + +const EMAIL_ERROR = 'Email:'; + +interface ErrorDescription { + description: string; +} + +interface ErrorDetails { + requester?: ErrorDescription[]; + base: ErrorDescription[]; +} + +// Errors are of the form: +// { +// "error": "RecordInvalid", +// "description": "Record validation errors", +// "details": { +// "requester": [ +// { +// "description": "Requester: Email: not_really_an_email.com is not properly formatted" +// } +// ] +// } +//} +const parseErrorMessage = (zendeskError?: Record): string => { + if (zendeskError?.details) { + const errorDetails = zendeskError.details as ErrorDetails; + const errorMessage = (errorDetails?.requester || errorDetails.base) + .map((error) => { + return error.description; + }) + .join(','); + if (errorMessage.indexOf(EMAIL_ERROR) !== -1) { + return 'report_issue.errors.invalid_email'; + } + return errorMessage.replace(':', '-'); // so localize doesn't mess with error message + } + return ''; // fallback to no error message, just an error title for our alert. +}; + +const reportIssue = async ({ + email, + name, + body, +}: ReportIssueProps): Promise => { + const requestBody = { + request: { + subject: ISSUE_SUBJECT, + requester: { name: name.trim().length > 0 ? name : ANONYMOUS, email }, + comment: { body }, + custom_fields: [ + { + [OS_FIELD_KEY]: Platform.OS, + [OS_VERSION_FIELD_KEY]: Platform.Version, + [APP_VERSION_FIELD_KEY]: getAppVersion(), + [APP_NAME_FIELD_KEY]: APP_NAME, + }, + ], + }, + }; + + const response = await fetch(env.ZENDESK_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const responseJson = await response.json(); + throw new Error(parseErrorMessage(responseJson)); + } +}; + +export default reportIssue; diff --git a/app/components/Button.tsx b/app/components/Button.tsx index ebaa9bef09..68dc797519 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -44,10 +44,13 @@ export const Button = ({ accessibilityRole='button' disabled={disabled || loading} style={[styles.button, style]}> - {loading ? ( - - ) : ( - {label} + {loading ? ' ' : label} + {loading && ( + )} ); diff --git a/app/helpers/getAppVersion.tsx b/app/helpers/getAppVersion.tsx new file mode 100644 index 0000000000..cbab45937d --- /dev/null +++ b/app/helpers/getAppVersion.tsx @@ -0,0 +1,16 @@ +import { getVersion, getBuildNumber } from 'react-native-device-info'; +import { isPlatformiOS } from '../Util'; + +const getAppVersion = (): string => { + const version = getVersion(); + // Append "ALPHA" to our iOS builds that are 1.0.0, as we use + // a separate Alpha TestFlight that is always 1.0.0. + // On android we include "ALPHA" directly in the version name. + const isAlpha = version === '1.0.0'; + const appVersion = `${ + isAlpha && isPlatformiOS() ? 'ALPHA ' : '' + }${version} (${getBuildNumber()})`; + return appVersion; +}; + +export default getAppVersion; diff --git a/app/locales/en.json b/app/locales/en.json index c4e7b52221..767d6373e5 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -18,7 +18,9 @@ "done": "Done", "next": "Next", "something_went_wrong": "Something went wrong", - "start": "Start" + "start": "Start", + "submit": "Submit", + "success": "Success" }, "export": { "code_input_body": "The representative from {{name}} will provide a verification code over the phone to link your data with {{name}}.", @@ -182,12 +184,22 @@ "notification_header": "Would you like to receive notifications when you may have crossed paths with the virus?", "notification_subheader": "PathCheck partners with local Health Departments to track the virus. If you are located in a jurisdiction served by one of our Health Department partners, you can receive exposure notifications from your Health Department." }, + "report_issue": { + "body": "Feedback (required)", + "email": "Email (required)", + "errors": { + "invalid_email": "The email seems to be invalid, please check again" + }, + "name": "Full Name", + "success": "We received your feedback. Thank you!" + }, "screen_titles": { "about": "About", "delete_location_history": "Delete Location History", "exposure_history": "Exposure History", "legal": "Legal", - "more_info": "More Info" + "more_info": "More Info", + "report_issue": "Report an Issue" }, "version_update": { "alert_label": "PathCheck is outdated", diff --git a/app/navigation/index.ts b/app/navigation/index.ts index 7a22d354e5..951c5f1378 100644 --- a/app/navigation/index.ts +++ b/app/navigation/index.ts @@ -50,7 +50,8 @@ export type Screen = | 'AffectedUserPublishConsent' | 'AffectedUserConfirmUpload' | 'AffectedUserExportDone' - | 'AffectedUserComplete'; + | 'AffectedUserComplete' + | 'ReportIssue'; export const Screens: { [key in Screen]: Screen } = { ExportStart: 'ExportStart', @@ -91,6 +92,7 @@ export const Screens: { [key in Screen]: Screen } = { AffectedUserConfirmUpload: 'AffectedUserConfirmUpload', AffectedUserExportDone: 'AffectedUserExportDone', AffectedUserComplete: 'AffectedUserComplete', + ReportIssue: 'ReportIssue', }; export type Stack = diff --git a/app/styles/typography.ts b/app/styles/typography.ts index 1663caba52..97721069df 100644 --- a/app/styles/typography.ts +++ b/app/styles/typography.ts @@ -211,7 +211,6 @@ export const primaryTextInput: TextStyle = { ...extraBold, fontSize: larger, lineHeight: largest, - textAlign: 'center', color: Colors.primaryText, }; diff --git a/app/views/About.js b/app/views/About.js index 19982d8871..ed590d7abc 100644 --- a/app/views/About.js +++ b/app/views/About.js @@ -10,24 +10,15 @@ import { Alert, TouchableWithoutFeedback, } from 'react-native'; -import { getVersion, getBuildNumber } from 'react-native-device-info'; import { NavigationBarWrapper, Typography } from '../components'; import { useDispatch } from 'react-redux'; import toggleAllowFeatureFlagsAction from '../store/actions/featureFlags/toggleAllowFeatureFlagsEnabledAction'; import { Colors, Spacing, Typography as TypographyStyles } from '../styles'; +import getAppVersion from '../helpers/getAppVersion'; const CLICKS_TO_ENABLE_FEATURE_FLAGS = 10; -const VERSION = getVersion(); - -// Append "ALPHA" to our iOS builds that are 1.0.0, as we use -// a separate Alpha TestFlight that is always 1.0.0. -// On android we include "ALPHA" directly in the version name. -const isAlpha = VERSION === '1.0.0'; -const APP_VERSION = `${ - isAlpha && Platform.OS === 'ios' ? 'ALPHA ' : '' -}${VERSION} (${getBuildNumber()})`; export const AboutScreen = ({ navigation }) => { const dispatch = useDispatch(); @@ -80,7 +71,7 @@ export const AboutScreen = ({ navigation }) => { - {APP_VERSION} + {getAppVersion()} diff --git a/app/views/ReportIssueForm.tsx b/app/views/ReportIssueForm.tsx new file mode 100644 index 0000000000..b4e184835e --- /dev/null +++ b/app/views/ReportIssueForm.tsx @@ -0,0 +1,185 @@ +import React, { useEffect, useState, FunctionComponent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + KeyboardAvoidingView, + StyleSheet, + TextInput, + View, + Keyboard, + Alert, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; + +import { Button } from '../components/Button'; +import reportIssue from '../api/zendesk/reportIssue'; + +import { + Spacing, + Forms, + Colors, + Outlines, + Typography as TypographyStyles, +} from '../styles'; +import { Typography } from '../components/Typography'; +import { NavigationBarWrapper } from '../components/NavigationBarWrapper'; +import { isPlatformiOS } from '../Util'; + +const ReportIssueForm: FunctionComponent = () => { + const { t } = useTranslation(); + const navigation = useNavigation(); + + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [body, setBody] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isDisabled, setIsDisabled] = useState(true); + + const validate = () => { + const hasEmail = email.trim().length > 0; + const hasBody = body.trim().length > 0; + + if (hasEmail && hasBody) { + setIsDisabled(false); + } else { + setIsDisabled(true); + } + }; + + useEffect(validate, [email, body]); + + const clearInputs = () => { + setBody(''); + setEmail(''); + setName(''); + }; + + const submitForm = async () => { + setIsLoading(true); + try { + await reportIssue({ + name, + email, + body, + }); + + clearInputs(); + setIsLoading(false); + Alert.alert(t('common.success'), t('report_issue.success'), [ + { onPress: navigation.goBack }, + ]); + } catch (e) { + Alert.alert(t('common.something_went_wrong'), t(e.message)); + setIsLoading(false); + } + setTimeout(() => setIsLoading(false), 1500); + }; + + return ( + + + + + + + + +