├── .watchmanconfig ├── .gitattributes ├── ios ├── .ruby-version ├── example.env ├── IntentionalWalkApp │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── Splash.imageset │ │ │ ├── iWalk.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── iwalk.png │ │ │ ├── iwalk120.png │ │ │ ├── iwalk180.png │ │ │ └── Contents.json │ ├── IntentionalWalkApp.entitlements │ ├── IntentionalWalkAppRelease.entitlements │ ├── main.m │ ├── AppDelegate.h │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.xib │ └── AppDelegate.m ├── IntentionalWalkApp-Bridging-Header.h ├── File.swift ├── IntentionalWalkApp.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── IntentionalWalkApp.xcscheme ├── IntentionalWalkApp.xcworkspace │ ├── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata ├── fastlane │ ├── Appfile │ ├── README.md │ └── Fastfile ├── IntentionalWalkAppTests │ ├── Info.plist │ └── IntentionalWalkAppTests.m └── Podfile ├── docs ├── _config.yml ├── index.md └── privacy.md ├── android ├── example.env ├── fastlane │ ├── metadata │ │ └── android │ │ │ └── en-US │ │ │ └── changelogs │ │ │ ├── 5.txt │ │ │ ├── 10.txt │ │ │ ├── 6.txt │ │ │ ├── 8.txt │ │ │ ├── 11.txt │ │ │ ├── 7.txt │ │ │ ├── 9.txt │ │ │ ├── 4.txt │ │ │ ├── 3.txt │ │ │ ├── 14.txt │ │ │ ├── 2.txt │ │ │ ├── 12.txt │ │ │ └── 13.txt │ ├── Appfile │ ├── README.md │ └── Fastfile ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── iwalk.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ └── layout │ │ │ │ │ └── launch_screen.xml │ │ │ ├── ic_launcher-web.png │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── roboto.ttf │ │ │ │ │ ├── roboto_bold.ttf │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ └── roboto_medium.ttf │ │ │ ├── java │ │ │ │ └── org │ │ │ │ │ └── codeforsanfrancisco │ │ │ │ │ └── intentionalwalk │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ ├── build_defs.bzl │ └── _BUCK ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── gradle.properties ├── build.gradle └── gradlew.bat ├── .env.dev ├── .env.prod ├── .env.staging ├── app.json ├── assets ├── logo.png ├── stop.png ├── pause.png ├── record.png ├── c4sf_logo.png ├── cdph_logo.png ├── gradient.png ├── logo_full.png ├── dolorespark.jpg ├── ribbon_big.png ├── sfdph_logo.png ├── top_walkers.png ├── calfresh_logo.png ├── sfgiants_logo.png ├── HomePageMyGoals.png ├── sfrecparks_logo.png ├── HomePageTopWalkers.png ├── HomePageWhereToWalk.png ├── top_walkers_trophy.png ├── check_circle_outline_gray.png ├── check_circle_outline_white.png ├── btn_google_signin_dark_normal_web.png ├── privacy │ ├── index.js │ ├── privacy.zh.txt │ ├── privacy.en.txt │ └── privacy.es.txt └── contestRules │ ├── index.js │ ├── contestRules.zh.txt │ ├── contestRules.en.txt │ └── contestRules.es.txt ├── styles ├── index.js ├── colors.js └── global.js ├── .eslintrc.js ├── routes ├── index.js ├── onboardingStack.js └── mainStack.js ├── .buckconfig ├── babel.config.js ├── .prettierrc.js ├── lib ├── util.js ├── index.js ├── validZipCodes.js ├── pedometer.ios.js ├── notifications.js ├── pedometer.android.js ├── api.js └── fitness.js ├── Gemfile ├── __tests__ └── App-test.js ├── components ├── linkButton.js ├── logo.js ├── hamburgerButton.js ├── pageTitle.js ├── index.js ├── paginationDots.js ├── checkBox.js ├── infoBox.js ├── multipleChoiceQuestion.js ├── statBox.js ├── popup.js ├── scrollText.js ├── button.js ├── input.js ├── multipleChoiceAnswer.js ├── dateNavigator.js ├── weekNavigator.js └── recordedWalk.js ├── screens ├── main │ ├── index.js │ ├── privacy.js │ ├── contestRules.js │ ├── recordedWalks.js │ ├── whereToWalk.js │ ├── about.js │ └── partners.js ├── onboarding │ ├── index.js │ ├── info.js │ ├── LoHOrigin.js │ ├── permissions.js │ ├── whatIsGenderIdentity.js │ ├── whatIsSexualOrientation.js │ ├── welcome.js │ └── whatIsRace.js └── tracker.js ├── index.js ├── metro.config.js ├── LICENSE.txt ├── .gitignore ├── App.js ├── .flowconfig └── package.json /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /ios/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.6 2 | -------------------------------------------------------------------------------- /ios/example.env: -------------------------------------------------------------------------------- 1 | APPLE_ID= 2 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /android/example.env: -------------------------------------------------------------------------------- 1 | UPLOAD_KEYSTORE_PASSWORD= 2 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | API_BASE_URL=http://localhost:8000 2 | ENV_NAME=Local 3 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | API_BASE_URL=https://iwalk-prod.herokuapp.com 2 | ENV_NAME=Production 3 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | API_BASE_URL=https://iwalk-staging.herokuapp.com 2 | ENV_NAME=Staging 3 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | [#21] First pass i18n and l10n (#59) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IntentionalWalkApp", 3 | "displayName": "IntentionalWalkApp" 4 | } -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/stop.png -------------------------------------------------------------------------------- /styles/index.js: -------------------------------------------------------------------------------- 1 | export Colors from './colors'; 2 | export GlobalStyles from './global'; 3 | -------------------------------------------------------------------------------- /assets/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/pause.png -------------------------------------------------------------------------------- /assets/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/record.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /assets/c4sf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/c4sf_logo.png -------------------------------------------------------------------------------- /assets/cdph_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/cdph_logo.png -------------------------------------------------------------------------------- /assets/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/gradient.png -------------------------------------------------------------------------------- /assets/logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/logo_full.png -------------------------------------------------------------------------------- /assets/dolorespark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/dolorespark.jpg -------------------------------------------------------------------------------- /assets/ribbon_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/ribbon_big.png -------------------------------------------------------------------------------- /assets/sfdph_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/sfdph_logo.png -------------------------------------------------------------------------------- /assets/top_walkers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/top_walkers.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | [#61] Fix Android pedometer observer callback invocation -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | [#21] Fix localized strings on walk recorder component -------------------------------------------------------------------------------- /assets/calfresh_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/calfresh_logo.png -------------------------------------------------------------------------------- /assets/sfgiants_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/sfgiants_logo.png -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | export MainStack from './mainStack'; 2 | export OnboardingStack from './onboardingStack'; 3 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /assets/HomePageMyGoals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/HomePageMyGoals.png -------------------------------------------------------------------------------- /assets/sfrecparks_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/sfrecparks_logo.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | iWalk 3 | 4 | -------------------------------------------------------------------------------- /assets/HomePageTopWalkers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/HomePageTopWalkers.png -------------------------------------------------------------------------------- /assets/HomePageWhereToWalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/HomePageWhereToWalk.png -------------------------------------------------------------------------------- /assets/top_walkers_trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/top_walkers_trophy.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | [#61] Android pedometer, only request activity samples upon step callback -------------------------------------------------------------------------------- /assets/check_circle_outline_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/check_circle_outline_gray.png -------------------------------------------------------------------------------- /assets/check_circle_outline_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/check_circle_outline_white.png -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/assets/fonts/roboto.ttf -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/11.txt: -------------------------------------------------------------------------------- 1 | [#62] New home tile design 2 | [#65] Add Email Us to menu 3 | [#56] Show build version in menu -------------------------------------------------------------------------------- /assets/btn_google_signin_dark_normal_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/assets/btn_google_signin_dark_normal_web.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: [['module:react-native-dotenv']], 4 | }; 5 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/roboto_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/assets/fonts/roboto_bold.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/iwalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/drawable-xxhdpi/iwalk.png -------------------------------------------------------------------------------- /ios/IntentionalWalkApp-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/roboto_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/assets/fonts/roboto_medium.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | [#61] Start step observer in addition to activity sample tracking in attempt to address Android tracking issues -------------------------------------------------------------------------------- /assets/privacy/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en: require('./privacy.en.txt'), 3 | es: require('./privacy.es.txt'), 4 | zh: require('./privacy.zh.txt'), 5 | }; 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #702B80 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | arrowParens: 'avoid', 7 | }; 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /assets/contestRules/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en: require('./contestRules.en.txt'), 3 | es: require('./contestRules.es.txt'), 4 | zh: require('./contestRules.zh.txt'), 5 | }; 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #702B80 4 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Images.xcassets/Splash.imageset/iWalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/ios/IntentionalWalkApp/Images.xcassets/Splash.imageset/iWalk.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Images.xcassets/AppIcon.appiconset/iwalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/ios/IntentionalWalkApp/Images.xcassets/AppIcon.appiconset/iwalk.png -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Images.xcassets/AppIcon.appiconset/iwalk120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/ios/IntentionalWalkApp/Images.xcassets/AppIcon.appiconset/iwalk120.png -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Images.xcassets/AppIcon.appiconset/iwalk180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfbrigade/intentional-walk/HEAD/ios/IntentionalWalkApp/Images.xcassets/AppIcon.appiconset/iwalk180.png -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | export function numberWithCommas(num) { 2 | return num !== undefined 3 | ? Math.round(num) 4 | .toString() 5 | .replace(/\B(?=(\d{3})+(?!\d))/g, ',') 6 | : 0; 7 | } 8 | -------------------------------------------------------------------------------- /ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // IntentionalWalkApp 4 | // 5 | // Created by Francis Li on 6/1/21. 6 | // Copyright © 2021 Facebook. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'IntentionalWalkApp' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export Api from './api'; 2 | export Fitness from './fitness'; 3 | export Notifications from './notifications'; 4 | export Pedometer from './pedometer'; 5 | export Realm from './realm'; 6 | export Strings from './strings'; 7 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("fastlane/key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name("org.codeforsanfrancisco.intentionalwalk") # e.g. com.krausefx.app 3 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | [#61] More defensive coding to try and address Android pedometer walk stat issues 2 | [#61] More defensive coding to try and address Android pedometer walk stat issues 3 | [#61] Attempt to ignore undefined data zeroing out walk stats -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # gem "rails" 8 | 9 | gem "fastlane", "~> 2.141" 10 | 11 | gem "cocoapods", "~> 1.11" 12 | 13 | gem "ffi", "~> 1.15" 14 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 01 10:54:04 MST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | [#11 #12] First pass, recording intentional walks 2 | [#28] More component and model set up for displaying Recorded Walks on Home screen and on dedicated screen 3 | [#28] Recorded walk component (#57) 4 | Create privacy.md 5 | Set theme jekyll-theme-minimal 6 | Create index.md -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | [#51] Add Realm datastore, store user after signup flow 2 | [#54] Android icon added 3 | [#50] Splash screen displayed upon launch 4 | [#10] Home screen stat box styling (#53) 5 | [#33] Add SF Rec & Parks logo to Where to Walk screen 6 | [#33] New components PageTitle, Link Button. Where to Walk screen content mostly completed (#52) -------------------------------------------------------------------------------- /ios/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("org.codeforsanfrancisco.intentionalwalk") # The bundle identifier of your app 2 | apple_id(ENV['APPLE_ID']) # Your Apple email address 3 | 4 | itc_team_id("121027814") # App Store Connect Team ID 5 | team_id("2XU846A9M4") # Developer Portal Team ID 6 | 7 | # For more information about the Appfile, see: 8 | # https://docs.fastlane.tools/advanced/#appfile 9 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/14.txt: -------------------------------------------------------------------------------- 1 | [#77] Client/Server API integration, new Contest model for dynamic contest dates 2 | [#98] Spanish translation of Zip Code Required alert 3 | [#99] Spanish translation of email required alert 4 | [#102] Capitalize month and day of week in date navigator 5 | [#103] Fix capitalization typo in Spanish strings 6 | [#55] Query steps/distance by month, not day -------------------------------------------------------------------------------- /components/linkButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Linking} from 'react-native'; 3 | import {Button} from './index'; 4 | 5 | export default function LinkButton({onHeight, style, title, url}) { 6 | return ( 7 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | [#19 #49] First pass, fastlane build automation, code reorganization into root of repo 2 | [#17] Hamburger menu in main screen stack with placeholders for other screens (#48) 3 | [#10] Refactor Fitness API access to get steps/total steps/distance (#47) 4 | [#20] Refactored and re-styled Onboarding screens (#46) 5 | [#35] User can navigate between days to see step history (#45) 6 | -------------------------------------------------------------------------------- /screens/main/index.js: -------------------------------------------------------------------------------- 1 | export AboutScreen from './about'; 2 | export ContestRulesScreen from './contestRules'; 3 | export GoalProgressScreen from './goalProgress'; 4 | export HomeScreen from './home'; 5 | export PartnersScreen from './partners'; 6 | export PrivacyScreen from './privacy'; 7 | export RecordedWalksScreen from './recordedWalks'; 8 | export TopWalkersScreen from './topWalkers'; 9 | export WhereToWalkScreen from './whereToWalk'; 10 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Images.xcassets/Splash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "iWalk.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/IntentionalWalkApp.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.healthkit 8 | 9 | com.apple.developer.healthkit.access 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/IntentionalWalkAppRelease.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | production 7 | com.apple.developer.healthkit 8 | 9 | com.apple.developer.healthkit.access 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /screens/onboarding/index.js: -------------------------------------------------------------------------------- 1 | export InfoScreen from './info'; 2 | export PermissionsScreen from './permissions'; 3 | export SignUpScreen from './signup'; 4 | export WelcomeScreen from './welcome'; 5 | 6 | export LoHOriginScreen from './LoHOrigin'; 7 | export WhatIsRaceScreen from './whatIsRace'; 8 | export WhatIsGenderIdentityScreen from './whatIsGenderIdentity'; 9 | export WhatIsSexualOrientationScreen from './whatIsSexualOrientation'; 10 | export SetYourStepGoal from './setYourStepGoal'; 11 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/launch_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | // https://reactnavigation.org/docs/getting-started/#installing-dependencies-into-a-bare-react-native-project 5 | // To finalize installation of react-native-gesture-handler, add the following at the top (make sure it's at the top and there's nothing else before it) of your entry file, such as index.js 6 | import 'react-native-gesture-handler'; 7 | import {AppRegistry} from 'react-native'; 8 | import App from './App'; 9 | import {name as appName} from './app.json'; 10 | 11 | AppRegistry.registerComponent(appName, () => App); 12 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/12.txt: -------------------------------------------------------------------------------- 1 | [#73] Resume button added to Stop/Finish state of Recording 2 | [#79] Sign up form validation alerts, including Age Restriction popup 3 | [#78] Privacy Policy popup from Sign up screen 4 | [#74] Updated app icon and launcher title 5 | [#67] Update translated text in localized strings file 6 | [#58] Attempt to address string cut-off issue on Oppo OnePlus Android phones 7 | [#70] User can navigate to Privacy Policy 8 | [#72] Updated logos and branding in app 9 | [#69] Program Partners screen 10 | [#68] Fix top header navigation 11 | -------------------------------------------------------------------------------- /components/logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Image, StyleSheet, View} from 'react-native'; 3 | 4 | export default function Logo(props) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | const styles = StyleSheet.create({ 12 | header: { 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | }, 16 | logo: { 17 | marginLeft: 20, 18 | marginRight: 20, 19 | width: 66, 20 | height: 16, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /android/fastlane/metadata/android/en-US/changelogs/13.txt: -------------------------------------------------------------------------------- 1 | [#80] Make side menu scrollable 2 | [#97] Updated Partners text 3 | [#96] Zip Code Required alert Chinese translation 4 | [#95] Email Required alert Chinese translation 5 | [#94] Change Chinese translation for Stop 6 | [#91] Space things out on iWalk Information 7 | [#90] Change Chinese translation for Where to Walk 8 | [#88] Reduce spacing of You're Signed Up screen 9 | [#84] Fix input placeholder text color 10 | [#85 #86 #89 #92] Various issue fixes 11 | [#83] Change steps/miles subtitle on previous days 12 | [#82] Updated logo asset 13 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | const {getDefaultConfig} = require('metro-config'); 8 | const defaultConfig = getDefaultConfig.getDefaultValues(__dirname); 9 | 10 | module.exports = { 11 | resolver: { 12 | assetExts: [...defaultConfig.resolver.assetExts, 'txt'], 13 | }, 14 | transformer: { 15 | getTransformOptions: async () => ({ 16 | transform: { 17 | experimentalImportSupport: false, 18 | inlineRequires: true, 19 | }, 20 | }), 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /styles/colors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | primary: { 3 | purple: '#702B80', 4 | lightGreen: '#86C03F', 5 | darkGreen: '#008F4D', 6 | gray2: '#4F4F4F', 7 | lightGray: '#F3F3F3', 8 | }, 9 | secondary: { 10 | blue: '#2B388A', 11 | red: '#E71C24', 12 | darkRed: '#AA2A30', 13 | gray3: '#7C7C7C', 14 | lightGray2: '#DADADA', 15 | }, 16 | accent: { 17 | orange: '#FA8554', 18 | teal: '#59CEC2', 19 | teal2: '#08A191', 20 | deepPurple: '#451B52', 21 | yellow: '#F7D211', 22 | blue: '#4D95B9', 23 | lightYellow: '#F8F2D7', 24 | lightPurple: '#CCB7D1', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /components/hamburgerButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, TouchableOpacity} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | import {Colors} from '../styles'; 5 | 6 | export default function HamburgerButton(props) { 7 | return ( 8 | props.onPress()}> 11 | 12 | 13 | ); 14 | } 15 | const styles = StyleSheet.create({ 16 | button: { 17 | width: 48, 18 | height: 48, 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /android/app/src/main/java/org/codeforsanfrancisco/intentionalwalk/MainActivity.java: -------------------------------------------------------------------------------- 1 | package org.codeforsanfrancisco.intentionalwalk; 2 | 3 | import android.os.Bundle; 4 | import com.facebook.react.ReactActivity; 5 | import org.devio.rn.splashscreen.SplashScreen; 6 | 7 | public class MainActivity extends ReactActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | SplashScreen.show(this); 11 | super.onCreate(savedInstanceState); 12 | } 13 | /** 14 | * Returns the name of the main component registered from JavaScript. This is used to schedule 15 | * rendering of the component. 16 | */ 17 | @Override 18 | protected String getMainComponentName() { 19 | return "IntentionalWalkApp"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Intentional Walk 2 | **Track your steps and compete for prizes** 3 | 4 | A free and easy-to-use mobile application as part of the Intentional Walk program, which will help participants stay motivated by tracking their steps while offering SF Giants swag as prize incentives. 5 | 6 | Intentional Walk is a program run by the San Francisco Department of Public Health, in partnership California Department of Public Health, SF Recreation and Parks Department, and the San Francisco Giants, to encourage San Francisco residents who are eligible for CalFresh/MediCal benefits to increase physical activity and develop healthy habits. 7 | 8 | Intentional Walk is an open source application developed by Code for San Francisco volunteers, a Code for America brigade. 9 | -------------------------------------------------------------------------------- /components/pageTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import {Colors, GlobalStyles} from '../styles'; 4 | 5 | export default function PageTitle({style, title}) { 6 | return ( 7 | 8 | {title} 9 | 10 | ); 11 | } 12 | const styles = StyleSheet.create({ 13 | content: { 14 | ...GlobalStyles.boxShadow, 15 | ...GlobalStyles.rounded, 16 | alignItems: 'center', 17 | backgroundColor: 'white', 18 | justifyContent: 'center', 19 | height: 64, 20 | marginBottom: 20, 21 | textAlign: 'center', 22 | }, 23 | title: { 24 | color: Colors.primary.purple, 25 | fontSize: 20, 26 | fontWeight: 'bold', 27 | letterSpacing: 0.5, 28 | textAlign: 'center', 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /ios/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## iOS 17 | 18 | ### ios beta 19 | 20 | ```sh 21 | [bundle exec] fastlane ios beta 22 | ``` 23 | 24 | Push a new beta build to TestFlight 25 | 26 | ---- 27 | 28 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 29 | 30 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 31 | 32 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 33 | -------------------------------------------------------------------------------- /ios/IntentionalWalkAppTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 41 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | platform :ios, '10.0' 5 | 6 | target 'IntentionalWalkApp' do 7 | config = use_native_modules! 8 | 9 | use_react_native!( 10 | :path => config[:reactNativePath], 11 | # to enable hermes on iOS, change `false` to `true` and then install pods 12 | :hermes_enabled => false 13 | ) 14 | 15 | target 'IntentionalWalkAppTests' do 16 | inherit! :complete 17 | # Pods for testing 18 | end 19 | 20 | # Enables Flipper. 21 | # 22 | # Note that if you have use_frameworks! enabled, Flipper will not work and 23 | # you should disable the next line. 24 | use_flipper!() 25 | 26 | post_install do |installer| 27 | react_native_post_install(installer) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/validZipCodes.js: -------------------------------------------------------------------------------- 1 | const validZipCodes = [ 2 | '94109', 3 | '94110', 4 | '94122', 5 | '94112', 6 | '94115', 7 | '94102', 8 | '94117', 9 | '94121', 10 | '94103', 11 | '94118', 12 | '94107', 13 | '94114', 14 | '94116', 15 | '94123', 16 | '94131', 17 | '94133', 18 | '94134', 19 | '94124', 20 | '94132', 21 | '94105', 22 | '94127', 23 | '94108', 24 | '94158', 25 | '94111', 26 | '94129', 27 | '94119', 28 | '94188', 29 | '94142', 30 | '94141', 31 | '94130', 32 | '94140', 33 | '94147', 34 | '94164', 35 | '94159', 36 | '94104', 37 | '94146', 38 | '94126', 39 | '94128', 40 | '94172', 41 | '94125', 42 | '94120', 43 | '94143', 44 | '94144', 45 | '94145', 46 | '94137', 47 | '94151', 48 | '94139', 49 | '94160', 50 | '94161', 51 | '94163', 52 | '94177', 53 | ]; 54 | export default validZipCodes; 55 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | export Button from './button'; 2 | export CheckBox from './checkBox'; 3 | export DateNavigator from './dateNavigator'; 4 | export HamburgerButton from './hamburgerButton'; 5 | export HamburgerMenu from './hamburgerMenu'; 6 | export InfoBox from './infoBox'; 7 | export Input from './input'; 8 | export LinkButton from './linkButton'; 9 | export Logo from './logo'; 10 | export MultipleChoiceQuestion from './multipleChoiceQuestion'; 11 | export MultipleChoiceAnswer from './multipleChoiceAnswer'; 12 | export PageTitle from './pageTitle'; 13 | export PaginationDots from './paginationDots'; 14 | export Popup from './popup'; 15 | export RecordedWalk from './recordedWalk'; 16 | export Recorder from './recorder'; 17 | export ScrollText from './scrollText'; 18 | export StatBox from './statBox'; 19 | export GoalBox from './goalBox'; 20 | export WeekNavigator from './weekNavigator'; 21 | -------------------------------------------------------------------------------- /components/paginationDots.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import {Colors} from '../styles'; 4 | 5 | export default function PaginationDots(props) { 6 | const totalPages = props.totalPages || 1; 7 | const currentPage = props.currentPage; 8 | const dots = []; 9 | for (let i = 0; i < totalPages; i++) { 10 | dots.push( 11 | , 15 | ); 16 | } 17 | return {dots}; 18 | } 19 | const styles = StyleSheet.create({ 20 | dots: { 21 | flexDirection: 'row', 22 | }, 23 | dot: { 24 | backgroundColor: '#DADADA', 25 | borderRadius: 4, 26 | width: 8, 27 | height: 8, 28 | margin: 2, 29 | }, 30 | currentDot: { 31 | backgroundColor: Colors.primary.purple, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /screens/tracker.js: -------------------------------------------------------------------------------- 1 | import {createRef} from 'react'; 2 | 3 | export const routeNameRef = createRef(); 4 | export const navigationRef = createRef(); 5 | 6 | export function getActiveRouteName(state) { 7 | const route = state.routes[state.index]; 8 | if (route.state) { 9 | // Dive into nested navigators 10 | return getActiveRouteName(route.state); 11 | } 12 | return route.name; 13 | } 14 | 15 | export function onStateChange(state) { 16 | const previousRouteName = routeNameRef.current; 17 | const currentRouteName = getActiveRouteName(state); 18 | if (previousRouteName !== currentRouteName) { 19 | // can do screen tracking here if desired 20 | // console.log(previousRouteName, currentRouteName); 21 | // Save the current route name for later comparision 22 | routeNameRef.current = currentRouteName; 23 | } 24 | } 25 | 26 | export function isActiveRoute(routeName) { 27 | return routeNameRef.current === routeName; 28 | } 29 | -------------------------------------------------------------------------------- /android/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## Android 17 | 18 | ### android test 19 | 20 | ```sh 21 | [bundle exec] fastlane android test 22 | ``` 23 | 24 | Runs all the tests 25 | 26 | ### android beta 27 | 28 | ```sh 29 | [bundle exec] fastlane android beta 30 | ``` 31 | 32 | Deploy a new Beta version to the Google Play internal track 33 | 34 | ---- 35 | 36 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 37 | 38 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 39 | 40 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 41 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useAndroidX=true 21 | android.enableJetifier=true 22 | org.gradle.jvmargs=-Xmx1024m 23 | -------------------------------------------------------------------------------- /screens/main/privacy.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {SafeAreaView, ScrollView, View} from 'react-native'; 3 | import loadLocalResource from 'react-native-local-resource'; 4 | import Autolink from 'react-native-autolink'; 5 | import {PageTitle} from '../../components'; 6 | import {GlobalStyles} from '../../styles'; 7 | import {Strings} from '../../lib'; 8 | 9 | import Privacy from '../../assets/privacy'; 10 | 11 | export default function PrivacyScreen({navigation}) { 12 | const [text, setText] = useState(); 13 | loadLocalResource(Privacy[Strings.getLanguage()]).then(newText => 14 | setText(newText), 15 | ); 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/pedometer.ios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Pedometer from '@t2tx/react-native-universal-pedometer'; 4 | import Realm from './realm'; 5 | 6 | function startUpdates(callback) { 7 | Realm.getCurrentWalk().then(walk => { 8 | if (walk) { 9 | Pedometer.startPedometerUpdatesFromDate(walk.start.getTime(), callback); 10 | } 11 | }); 12 | } 13 | 14 | function stopUpdates() { 15 | Pedometer.stopPedometerUpdates(); 16 | } 17 | 18 | function getPedometerData(end) { 19 | return new Promise((resolve, reject) => { 20 | Realm.getCurrentWalk().then(walk => { 21 | if (walk) { 22 | Pedometer.queryPedometerDataBetweenDates( 23 | walk.start.getTime(), 24 | end.getTime(), 25 | (error, data) => { 26 | if (error) { 27 | reject(error); 28 | } else { 29 | resolve(data); 30 | } 31 | }, 32 | ); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | export default { 39 | startUpdates, 40 | getPedometerData, 41 | stopUpdates, 42 | }; 43 | -------------------------------------------------------------------------------- /assets/contestRules/contestRules.zh.txt: -------------------------------------------------------------------------------- 1 | 參賽資格 2 | 3 | * 該計劃向符合CalFresh/Medi-Cal 資格San Francisco居民開放。 4 | 5 | 參賽方式 6 | 7 | * 下載免費的Intentional Walk(有意圖的行走)應用程序。 8 | * 在應用程式商店中搜尋「Intentional Walk」。 9 | * 遵循註冊介面的指示,創建帳戶。 10 | * 開始行走!您可選擇性地記錄您的步數。這款應用程式將透過您手機上的保健應用程式(Apple Health 或Google Fit)自動追蹤您的步數。 11 | * 如需更多資訊,請造訪: iwalk.c4sf.me 12 | 13 | 計劃日期 14 | 15 | * 開始日期:2023 年6 月1 日,太平洋時間凌晨0:00。 16 | * 結束日期:2023 年8 月31 日,太平洋時間晚上11:59。 17 | 18 | 獎項設置 19 | 20 | * 在計劃結束時,位於步數排行榜前10 名的參賽者將收到電郵通知,以領取相應獎勵。 21 | * 除非另有說明,若獲勝者未在三 (3) 天內給出回覆,獎項將頒發給排行榜中的下一名參賽者。 22 | * 請注意:參賽者每次打開這款應用程式時,應用程式將自動更新其步數。使用者在打開應用程式時,步數排行榜亦將自動更新。如需將您所有的步數計算在內,請在競賽最後一日(2023 年8 月31 日)打開應用程式。 23 | 24 | 雜項 25 | 26 | * Intentional Walk 由San Francisco 公共衛生局運營,並獲美國農業部 (United States Department of Agriculture, USDA) 補充營養援助計劃 (Supplemental Nutrition Assistance Program, SNAP) 的贊助支持。USDA 是一個平等機會的提供者和雇主。社區合作夥伴包括加州公共衛生局、Code for San Francisco、San Francisco Giants 以及San Francisco 娛樂與公園管理局。 27 | * San Francisco 公共衛生局保留更改比賽規則、計劃日期和獎項頒發日期的權利。 28 | * 您可從隱私政策中找到關於隱私的資訊。 29 | * Apple 和Google 並非此次競賽的贊助商,不對比賽規則及結果負責。 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Code for San Francisco, A Code for America Brigade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "iwalk120.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "iwalk180.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "1024x1024", 47 | "idiom" : "ios-marketing", 48 | "filename" : "iwalk.png", 49 | "scale" : "1x" 50 | } 51 | ], 52 | "info" : { 53 | "version" : 1, 54 | "author" : "xcode" 55 | } 56 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | */fastlane/key.json 55 | */fastlane/metadata/android/en-US/changelogs/* 56 | 57 | # Bundle artifact 58 | *.jsbundle 59 | 60 | # CocoaPods 61 | /ios/Pods/ 62 | 63 | .env 64 | *.local 65 | *.cer 66 | *.certSigningRequest 67 | *.p12 68 | *.dSYM.zip 69 | *.mobileprovision 70 | -------------------------------------------------------------------------------- /components/checkBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View} from 'react-native'; 3 | import {Colors} from '../styles'; 4 | import {CheckBox} from 'react-native-elements'; 5 | 6 | export default function CustomCheckBox(props) { 7 | return ( 8 | 11 | props.onPress()} 25 | /> 26 | {props.children} 27 | 28 | ); 29 | } 30 | const styles = StyleSheet.create({ 31 | row: { 32 | flexDirection: 'row', 33 | alignItems: 'center', 34 | justifyContent: 'flex-start', 35 | marginBottom: 16, 36 | }, 37 | container: { 38 | backgroundColor: 'transparent', 39 | borderWidth: 0, 40 | padding: 0, 41 | margin: 0, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /styles/global.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {StyleSheet} from 'react-native'; 4 | import Colors from './colors'; 5 | 6 | export default StyleSheet.create({ 7 | androidNavHeaderCentered: { 8 | left: 0, 9 | width: '100%', 10 | }, 11 | rounded: { 12 | borderRadius: 10, 13 | }, 14 | container: { 15 | flex: 1, 16 | backgroundColor: Colors.primary.lightGray, 17 | }, 18 | content: { 19 | padding: 16, 20 | }, 21 | centered: { 22 | alignItems: 'center', 23 | }, 24 | boxShadow: { 25 | shadowColor: 'black', 26 | shadowOffset: {width: 4, height: 4}, 27 | shadowOpacity: 0.1, 28 | shadowRadius: 10, 29 | elevation: 10, 30 | }, 31 | h1: { 32 | color: Colors.primary.purple, 33 | fontSize: 36, 34 | fontWeight: '500', 35 | textAlign: 'center', 36 | marginBottom: 16, 37 | }, 38 | h2: { 39 | color: Colors.primary.gray2, 40 | fontSize: 20, 41 | fontWeight: 'bold', 42 | textAlign: 'left', 43 | marginBottom: 10, 44 | }, 45 | p1: { 46 | color: Colors.primary.gray2, 47 | fontSize: 12, 48 | textAlign: 'center', 49 | marginBottom: 16, 50 | }, 51 | p2: { 52 | color: Colors.primary.gray2, 53 | fontSize: 17, 54 | lineHeight: 20, 55 | textAlign: 'left', 56 | marginBottom: 10, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /components/infoBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Image, StyleSheet, Text, View} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | import {GlobalStyles} from '../styles'; 5 | 6 | export default function InfoBox(props) { 7 | return ( 8 | 9 | 10 | {props.icon && ( 11 | 17 | )} 18 | {props.image && } 19 | 20 | 21 | {props.title ? ( 22 | {props.title} 23 | ) : null} 24 | {props.children && ( 25 | {props.children} 26 | )} 27 | 28 | 29 | ); 30 | } 31 | 32 | const styles = StyleSheet.create({ 33 | container: { 34 | flexDirection: 'row', 35 | justifyContent: 'center', 36 | alignItems: 'flex-start', 37 | marginBottom: 16, 38 | }, 39 | icon: { 40 | width: 100, 41 | alignItems: 'center', 42 | }, 43 | text: { 44 | flex: 1, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /android/app/_BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "org.codeforsanfrancisco.intentionalwalk", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "org.codeforsanfrancisco.intentionalwalk", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /components/multipleChoiceQuestion.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import {Colors, GlobalStyles} from '../styles'; 4 | 5 | export default function MultipleChoiceQuestion(props) { 6 | return ( 7 | 8 | 9 | {props.text} 10 | {props.subText ? ( 11 | {props.subText} 12 | ) : ( 13 | <> 14 | )} 15 | 16 | {props.children} 17 | 18 | ); 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | wrapper: { 23 | width: '100%', 24 | }, 25 | content: { 26 | minHeight: 68, 27 | width: '100%', 28 | paddingTop: 0, 29 | paddingBottom: 2, 30 | paddingLeft: 28, 31 | paddingRight: 28, 32 | marginTop: 0, 33 | marginBottom: 2, 34 | borderRadius: GlobalStyles.rounded.borderRadius, 35 | justifyContent: 'center', 36 | backgroundColor: Colors.primary.purple, 37 | shadowColor: GlobalStyles.boxShadow.shadowColor, 38 | shadowOffset: GlobalStyles.boxShadow.shadowOffset, 39 | shadowOpacity: GlobalStyles.boxShadow.shadowOpacity, 40 | shadowRadius: GlobalStyles.boxShadow.shadowRadius, 41 | elevation: GlobalStyles.boxShadow.elevation, 42 | }, 43 | text: { 44 | fontWeight: 'bold', 45 | fontSize: 20, 46 | color: Colors.primary.lightGray, 47 | }, 48 | subText: { 49 | fontWeight: '500', 50 | fontSize: 17, 51 | color: Colors.primary.lightGray, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /components/statBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ActivityIndicator, StyleSheet, View, Text} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | 5 | export default function StatBox(props) { 6 | return ( 7 | 8 | 9 | {props.mainText === ' ' && ( 10 | 15 | )} 16 | 17 | {props.mainText} 18 | {props.mainTextSuffix !== '' ? ( 19 | {props.mainTextSuffix} 20 | ) : ( 21 | '' 22 | )} 23 | 24 | {props.subText} 25 | 30 | 31 | 32 | ); 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | box: { 37 | justifyContent: 'center', 38 | alignItems: 'center', 39 | height: 112, 40 | }, 41 | innerBox: { 42 | width: '100%', 43 | overflow: 'hidden', 44 | }, 45 | spinner: { 46 | position: 'absolute', 47 | }, 48 | mainText: { 49 | color: 'white', 50 | fontSize: 40, 51 | fontWeight: 'bold', 52 | paddingTop: 15, 53 | }, 54 | subText: { 55 | color: 'white', 56 | fontSize: 18, 57 | }, 58 | icon: { 59 | position: 'absolute', 60 | opacity: 0.15, 61 | color: 'white', 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /components/popup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, TouchableOpacity, View} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | 5 | import {GlobalStyles, Colors} from '../styles'; 6 | 7 | export default function Popup(props) { 8 | return ( 9 | 15 | 16 | 17 | 18 | 19 | 20 | {props.children} 21 | 22 | 23 | ); 24 | } 25 | 26 | const styles = StyleSheet.create({ 27 | container: { 28 | top: 0, 29 | right: 0, 30 | bottom: 0, 31 | left: 0, 32 | justifyContent: 'center', 33 | }, 34 | backdrop: { 35 | position: 'absolute', 36 | top: 0, 37 | right: 0, 38 | bottom: 0, 39 | left: 0, 40 | backgroundColor: Colors.primary.gray2, 41 | opacity: 0.4, 42 | width: '100%', 43 | height: '100%', 44 | }, 45 | box: { 46 | ...GlobalStyles.boxShadow, 47 | shadowOpacity: 0.25, 48 | backgroundColor: 'white', 49 | width: '90%', 50 | alignSelf: 'center', 51 | padding: 2, 52 | }, 53 | show: { 54 | display: 'flex', 55 | position: 'absolute', 56 | }, 57 | hide: { 58 | display: 'none', 59 | position: 'relative', 60 | }, 61 | closeIcon: { 62 | alignSelf: 'flex-end', 63 | }, 64 | content: { 65 | padding: 8, 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /assets/privacy/privacy.zh.txt: -------------------------------------------------------------------------------- 1 | 致Intentional Walk 應用程式使用者: 2 | 3 | 感謝您參加三藩市公共衛生局 (San Francisco Department of Public Health, SFDPH) 和加州公共衛生局 (California Department of Public Health, CDPH) 共同開展的 Intentional Walk計劃(意為「有意圖的行走計劃」,下稱「計劃」)並使用 Intentional Walk應用程式(下稱「應用程式」)。我們想告知您,參與計劃即表示您同意SFDPH/CDPH 和Code for San Francisco 獲取以下資料:比賽期間和比賽開始日期前30 天內的被動步數(未記錄的步數)、主動步數(已記錄的步數)、總步數和已記錄的步行距離,以及使用者的姓名、電子郵箱、年齡、居住地郵遞區號、種族、性取向和性別認同。當您解除安裝應用程式和/或計劃結束後,我們將不再收集其他數據。為保護您的個人資訊,SFDPH/CDPH 遵守以下規則和條例: 4 | 5 | 1. 僅可透過本文中披露的合法手段取得個人識別資訊。 6 | 7 | 2. 除非徵得使用者同意或法律法規要求,否則SFDPH/CDPH 和 Code for San Francisco不得以規定以外之任何目的或理由(包括第三方)提供、出售或使用個人資料。 8 | 9 | 3. 根據《公共檔案法》,不得請求取得以電子方式收集的個人資訊。 10 | 11 | 4. 收集任何個人資訊之目的均應與計劃的需要或目的相關。 12 | 13 | 5. 僅允許數量有限之人員存取個人資訊,該等人員對此類系統擁有特殊存取權,且要求該人員對此資訊保密。 14 | 15 | 6. 此外,為確保您個人資訊的安全,SFDPH/CDPH 在使用者輸入、提交或存取資訊時均採取安全措施。使用安全的HTTPS 加密協議將資料傳輸至服務器。 16 | 17 | 7. SFDPH/CDPH 遵循聯邦指南規定中有關保護個人資訊使用的規則。SFDPH/CDPH 嚴格遵循這些指南。 18 | 19 | 8. 應用程式收集個人資訊(包括使用者的姓名、年齡、郵遞區號、電子郵箱、種族、性取向和性別認同),以便在系統上建立帳戶,從而將行動存取連接至正確的使用者記錄。應用程式不會存儲使用者行走的實際位置。SFDPH/CDPH 將在應用程式中收集的資料用於計劃改進和評估,這些資料還可能包括行動設備類型、作業系統版本、設備使用的電訊廠商或網路以及在應用程式內造訪的頁面。使用者可聯絡SFDPH 並要求刪除自己的資料(聯絡資訊如下)。 20 | 21 | 9. 應用程式將保留在使用者的設備上,直至刪除。可透過解除安裝移動應用程式來刪除應用程式。 22 | 23 | 10. 只有當使用者同意應用程式存取移動設備的健康追蹤器(iOS 使用者的Apple Health 和Android 使用者的Google Fit),FDPH/CDPH 才可收集步行和步數的相關資料。應用程式將收集以前的使用者(目前已登記並且在前幾年參加過計劃的使用)的步行和步數相關資料,將該資料與其今年的資料相關聯,並用於評估。 24 | 25 | 11. 應用程式使用者將自動加入Top Walkers 清單(亦稱步數排行榜)。雖然應用程式使用者不能選擇退出Top Walkers 清單,但使用者將保持匿名;不會顯示姓名。 26 | 27 | 12. 其他方不會收集應用程式使用者在使用應用程式期間的線上活動以及在不同網站上的個人資訊。 28 | 29 | 13. 如果您對此資訊、Intentional Walk 計劃或應用程式有任何疑問或意見,請聯絡SFDPH:intentionalwalk@sfdph.org 或CDPH 隱私辦公室:Privacy@cdph.ca.gov。 30 | 31 | 14. 應用程式使用者有權要求SFDPH/CDPH 刪除任何以電子形式收集的個人資訊,同時不得重複使用或散佈資訊。應用程式使用者如欲刪除以電子形式收集的個人資訊,可聯絡SFDPH:intentionalwalk@sfdph.org。 -------------------------------------------------------------------------------- /assets/contestRules/contestRules.en.txt: -------------------------------------------------------------------------------- 1 | Who Can Enter 2 | 3 | * The program is open to CalFresh/Medi-Cal eligible San Francisco residents. 4 | 5 | How to Enter 6 | 7 | * Download the FREE Intentional Walk App to join. 8 | * In your app store, search, “Intentional Walk.” 9 | * Follow the instructions in the sign-up screens to create a profile. 10 | * Start walking! It is optional to record your walks. The app will automatically track your steps via your phone’s health application (Apple Health or Google Fit). 11 | * For more information: iwalk.c4sf.me 12 | 13 | Program Dates 14 | 15 | * Starts: 06/01/2023, 12:00 AM PST. 16 | * Ends: 08/31/2023, 11:59 PM PST. 17 | 18 | The Prize(s) 19 | 20 | * At the end of the program, the top 10 walkers will be contacted by email to claim their prize. 21 | * Unless otherwise stated, if a winner does not respond within three (3) days, we will award the prize to the next top walker. 22 | * Please note: The app updates a walker’s step count each time they open the app. The Top Walkers list is updated anytime a user opens the app. To have all of your steps counted, open the app on the final day of the contest, 08/31/2023. 23 | 24 | Miscellaneous 25 | 26 | * Intentional Walk is run by the SF Department of Public Health. It is funded by USDA SNAP, an equal opportunity provider and employer. Community partners include the California Department of Public Health, Code for San Francisco, San Francisco Giants, and the SF Recreation and Parks Department. 27 | * San Francisco Department of Public Health reserves the right to make rule changes and change the program and prize dates. 28 | * Privacy information can be found within the Privacy Policy. 29 | * Apple and Google are not sponsors for this competition and are not responsible for the rules and results. -------------------------------------------------------------------------------- /components/scrollText.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Image, ScrollView, StyleSheet, View} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | 5 | import {Colors} from '../styles'; 6 | 7 | export default function ScrollText(props) { 8 | const [showIndicator, setShowIndicator] = useState(true); 9 | 10 | const onScroll = ({nativeEvent}) => { 11 | const {contentOffset, contentSize, layoutMeasurement} = nativeEvent; 12 | setShowIndicator( 13 | contentOffset.y < contentSize.height - layoutMeasurement.height - 20, 14 | ); 15 | }; 16 | 17 | return ( 18 | 19 | 23 | {props.children} 24 | 25 | 31 | 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | const styles = StyleSheet.create({ 42 | scrollView: { 43 | width: '100%', 44 | height: '100%', 45 | }, 46 | scrollIndicator: { 47 | position: 'absolute', 48 | bottom: 0, 49 | width: '100%', 50 | alignItems: 'center', 51 | }, 52 | scrollIndicatorHidden: { 53 | position: 'relative', 54 | display: 'none', 55 | }, 56 | scrollIndicatorBackground: { 57 | position: 'absolute', 58 | bottom: 0, 59 | width: '100%', 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | authVersion = '18.0.0' 10 | fitnessVersion = '20.0.0' 11 | googlePlayServicesVersion = '17.0.0' 12 | firebaseMessagingVersion = "21.1.0" 13 | } 14 | repositories { 15 | maven { 16 | url = uri("https://plugins.gradle.org/m2/") 17 | } 18 | google() 19 | jcenter() 20 | } 21 | dependencies { 22 | classpath("com.android.tools.build:gradle:3.4.2") 23 | classpath("co.uzzu.dotenv:gradle:2.0.0") 24 | 25 | // NOTE: Do not place your application dependencies here; they belong 26 | // in the individual module build.gradle files 27 | } 28 | } 29 | apply plugin: "co.uzzu.dotenv.gradle" 30 | 31 | def REACT_NATIVE_VERSION = new File(['node', '--print',"JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim()) 32 | 33 | allprojects { 34 | configurations.all { 35 | resolutionStrategy { 36 | force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION 37 | } 38 | } 39 | 40 | repositories { 41 | mavenLocal() 42 | maven { 43 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 44 | url("$rootDir/../node_modules/react-native/android") 45 | } 46 | maven { 47 | // Android JSC is installed from npm 48 | url("$rootDir/../node_modules/jsc-android/dist") 49 | } 50 | 51 | google() 52 | jcenter() 53 | maven { url 'https://jitpack.io' } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /components/button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, TouchableOpacity, Text} from 'react-native'; 3 | import {Colors, GlobalStyles} from '../styles'; 4 | 5 | export default function Button(props) { 6 | function onLayout({nativeEvent}) { 7 | if (props.onHeight) { 8 | props.onHeight(nativeEvent.layout.height); 9 | } 10 | } 11 | return ( 12 | props.onPress()}> 22 | {React.Children.map(props.children, c => 23 | typeof c === 'string' ? ( 24 | 30 | {c} 31 | 32 | ) : ( 33 | c 34 | ), 35 | )} 36 | 37 | ); 38 | } 39 | const styles = StyleSheet.create({ 40 | button: { 41 | ...GlobalStyles.rounded, 42 | alignItems: 'center', 43 | justifyContent: 'center', 44 | backgroundColor: 'purple', 45 | minHeight: 48, 46 | marginBottom: 16, 47 | padding: 10, 48 | }, 49 | buttonToggle: { 50 | backgroundColor: 'white', 51 | borderColor: Colors.primary.purple, 52 | borderWidth: 0.5, 53 | }, 54 | buttonDisabled: { 55 | borderWidth: 0, 56 | backgroundColor: '#DADADA', 57 | }, 58 | text: { 59 | color: 'white', 60 | fontSize: 24, 61 | lineHeight: 28, 62 | fontWeight: '500', 63 | textAlign: 'center', 64 | }, 65 | textToggle: { 66 | color: Colors.primary.purple, 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Platform, Text} from 'react-native'; 3 | import {SafeAreaProvider} from 'react-native-safe-area-context'; 4 | import {NavigationContainer} from '@react-navigation/native'; 5 | import {createStackNavigator} from '@react-navigation/stack'; 6 | import {MainStack, OnboardingStack} from './routes'; 7 | import {navigationRef, routeNameRef, onStateChange} from './screens/tracker'; 8 | 9 | /// load locales, set defaults 10 | import {Strings} from './lib'; 11 | import moment from 'moment'; 12 | import 'moment/locale/es'; 13 | import 'moment/locale/zh-cn'; 14 | moment.locale(Strings.getLanguage()); 15 | 16 | /// https://github.com/facebook/react-native/issues/15114 17 | /// hack for Android phones with non-standard fonts 18 | if (Platform.OS === 'android') { 19 | const oldRender = Text.render; 20 | Text.render = function (...args) { 21 | const origin = oldRender.call(this, ...args); 22 | return React.cloneElement(origin, { 23 | style: [{fontFamily: 'roboto'}, origin.props.style], 24 | }); 25 | }; 26 | } 27 | 28 | const RootStack = createStackNavigator(); 29 | 30 | /// initialize first route 31 | routeNameRef.current = 'Home'; 32 | 33 | const App: () => React$Node = () => { 34 | return ( 35 | 36 | onStateChange(state)}> 39 | 40 | 45 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | 56 | export default App; 57 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore polyfills 9 | node_modules/react-native/Libraries/polyfills/.* 10 | 11 | ; These should not be required directly 12 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 13 | node_modules/warning/.* 14 | 15 | ; Flow doesn't support platforms 16 | .*/Libraries/Utilities/LoadingView.js 17 | 18 | [untyped] 19 | .*/node_modules/@react-native-community/cli/.*/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/interface.js 25 | node_modules/react-native/flow/ 26 | 27 | [options] 28 | emoji=true 29 | 30 | esproposal.optional_chaining=enable 31 | esproposal.nullish_coalescing=enable 32 | 33 | exact_by_default=true 34 | 35 | module.file_ext=.js 36 | module.file_ext=.json 37 | module.file_ext=.ios.js 38 | 39 | munge_underscores=true 40 | 41 | module.name_mapper='^react-native$' -> '/node_modules/react-native/Libraries/react-native/react-native-implementation' 42 | module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' 43 | module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' 44 | 45 | suppress_type=$FlowIssue 46 | suppress_type=$FlowFixMe 47 | suppress_type=$FlowFixMeProps 48 | suppress_type=$FlowFixMeState 49 | 50 | [lints] 51 | sketchy-null-number=warn 52 | sketchy-null-mixed=warn 53 | sketchy-number=warn 54 | untyped-type-import=warn 55 | nonstrict-import=warn 56 | deprecated-type=warn 57 | unsafe-getters-setters=warn 58 | unnecessary-invariant=warn 59 | signature-verification-failure=warn 60 | 61 | [strict] 62 | deprecated-type 63 | nonstrict-import 64 | sketchy-null 65 | unclear-type 66 | unsafe-getters-setters 67 | untyped-import 68 | untyped-type-import 69 | 70 | [version] 71 | ^0.137.0 72 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | iWalk 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 41 25 | ITSAppUsesNonExemptEncryption 26 | 27 | LSRequiresIPhoneOS 28 | 29 | NSAppTransportSecurity 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSHealthShareUsageDescription 41 | To track your steps. 42 | NSHealthUpdateUsageDescription 43 | To track your steps. 44 | NSLocationWhenInUseUsageDescription 45 | 46 | NSMotionUsageDescription 47 | To track your steps in realtime. 48 | UIAppFonts 49 | 50 | MaterialIcons.ttf 51 | 52 | UILaunchStoryboardName 53 | LaunchScreen 54 | UIRequiredDeviceCapabilities 55 | 56 | armv7 57 | 58 | UISupportedInterfaceOrientations 59 | 60 | UIInterfaceOrientationPortrait 61 | 62 | UIViewControllerBasedStatusBarAppearance 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /screens/main/contestRules.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {SafeAreaView, ScrollView, View} from 'react-native'; 3 | import loadLocalResource from 'react-native-local-resource'; 4 | import Autolink from 'react-native-autolink'; 5 | import {PageTitle} from '../../components'; 6 | import {GlobalStyles} from '../../styles'; 7 | import {Realm, Strings} from '../../lib'; 8 | import moment from 'moment'; 9 | 10 | import ContestRules from '../../assets/contestRules'; 11 | 12 | export default function ContestRulesScreen({navigation}) { 13 | const [text, setText] = useState(); 14 | const [contest, setContest] = useState(null); 15 | 16 | useEffect(() => { 17 | Realm.getContest().then(newContest => 18 | setContest(newContest ? newContest.toObject() : null), 19 | ); 20 | }, []); 21 | 22 | let from = null, 23 | to = null, 24 | fromEn = null, 25 | toEn = null; 26 | // English Contest Rules use long form of date (rangeTo format) 27 | // Other languages use short form (dateSlash format) 28 | if (contest) { 29 | from = moment(contest.start).format(Strings.common.dateSlash); 30 | to = moment(contest.end).format(Strings.common.dateSlash); 31 | fromEn = moment(contest.start).format(Strings.common.rangeTo); 32 | toEn = moment(contest.end).format(Strings.common.rangeTo); 33 | } else { 34 | // default value just in case contest is unavailable 35 | from = '09/01/2021'; 36 | to = '09/30/2021'; 37 | fromEn = 'September 1, 2021'; 38 | toEn = 'September 30, 2021'; 39 | } 40 | 41 | loadLocalResource(ContestRules[Strings.getLanguage()]).then(newText => 42 | setText(newText), 43 | ); 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /screens/main/recordedWalks.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {VirtualizedList, SafeAreaView, StyleSheet} from 'react-native'; 3 | import {PageTitle, RecordedWalk} from '../../components'; 4 | import {GlobalStyles} from '../../styles'; 5 | import {Realm, Strings} from '../../lib'; 6 | 7 | export default function RecordedWalksScreen({navigation}) { 8 | const [recordedWalks, setRecordedWalks] = useState(null); 9 | 10 | useEffect(() => { 11 | Realm.getWalks().then(walks => setRecordedWalks(walks)); 12 | /// also synchronize with server 13 | Realm.syncWalks(); 14 | }, []); 15 | 16 | return ( 17 | 18 | {recordedWalks && ( 19 | data.length + 1} 23 | getItem={(data, i) => (i === 0 ? {id: ''} : data[i - 1])} 24 | renderItem={({item}) => { 25 | if (item.id !== '') { 26 | return ( 27 | 28 | ); 29 | } else { 30 | return ( 31 | <> 32 | 36 | {recordedWalks.length === 0 && ( 37 | 41 | )} 42 | 43 | ); 44 | } 45 | }} 46 | keyExtractor={item => item.id} 47 | /> 48 | )} 49 | 50 | ); 51 | } 52 | 53 | const styles = StyleSheet.create({ 54 | pageTitle: { 55 | marginTop: 16, 56 | marginLeft: 16, 57 | marginRight: 16, 58 | marginBottom: 8, 59 | }, 60 | list: {}, 61 | walk: { 62 | marginTop: 8, 63 | marginBottom: 8, 64 | marginLeft: 16, 65 | marginRight: 16, 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /components/input.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react'; 2 | import {StyleSheet, TextInput} from 'react-native'; 3 | import {Colors, GlobalStyles} from '../styles'; 4 | 5 | export default function Input(props) { 6 | const prevFocusRef = useRef(); 7 | const textInputRef = useRef(null); 8 | const [value, setValue] = useState(props.value || ''); 9 | 10 | const onChangeText = newValue => { 11 | setValue(newValue); 12 | if (props.onChangeText) { 13 | props.onChangeText(newValue); 14 | } 15 | }; 16 | 17 | useEffect(() => { 18 | if (prevFocusRef.current !== props.focused && props.focused) { 19 | textInputRef.current.focus(); 20 | } 21 | prevFocusRef.current = props.focused; 22 | }); 23 | 24 | return ( 25 | onChangeText(newValue)} 35 | onSubmitEditing={nativeEvent => 36 | props.onSubmitEditing ? props.onSubmitEditing(nativeEvent) : null 37 | } 38 | placeholder={props.placeholder} 39 | placeholderTextColor={props.placeholderTextColor || Colors.primary.gray2} 40 | autoCapitalize={props.autoCapitalize || 'none'} 41 | autoCompleteType={props.autoCompleteType || 'off'} 42 | autoCorrect={props.autoCorrect || false} 43 | keyboardType={props.keyboardType || 'default'} 44 | returnKeyType={props.returnKeyType || 'done'} 45 | /> 46 | ); 47 | } 48 | const styles = StyleSheet.create({ 49 | input: { 50 | ...GlobalStyles.rounded, 51 | width: '100%', 52 | height: 56, 53 | backgroundColor: 'white', 54 | borderColor: Colors.primary.gray2, 55 | borderWidth: 0.5, 56 | marginBottom: 16, 57 | fontSize: 17, 58 | paddingLeft: 12, 59 | paddingRight: 12, 60 | color: Colors.primary.purple, 61 | }, 62 | inputFocused: { 63 | borderColor: Colors.primary.purple, 64 | }, 65 | inputDisabled: { 66 | color: Colors.primary.gray2, 67 | borderColor: Colors.primary.gray2, 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /routes/onboardingStack.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Platform} from 'react-native'; 3 | import {createStackNavigator} from '@react-navigation/stack'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import { 6 | WelcomeScreen, 7 | SignUpScreen, 8 | InfoScreen, 9 | PermissionsScreen, 10 | LoHOriginScreen, 11 | WhatIsRaceScreen, 12 | WhatIsGenderIdentityScreen, 13 | WhatIsSexualOrientationScreen, 14 | SetYourStepGoal, 15 | } from '../screens/onboarding'; 16 | import {GoalProgressScreen} from '../screens/main'; 17 | import {Logo} from '../components'; 18 | import {Colors, GlobalStyles} from '../styles'; 19 | import {Strings} from '../lib'; 20 | 21 | const Stack = createStackNavigator(); 22 | 23 | export default function OnboardingStack() { 24 | return ( 25 | ( 29 | 30 | ), 31 | headerBackTitle: Strings.common.back.toUpperCase(), 32 | headerBackTitleVisible: true, 33 | headerTintColor: Colors.primary.purple, 34 | headerTitle: props => , 35 | headerTitleContainerStyle: Platform.select({ 36 | android: GlobalStyles.androidNavHeaderCentered, 37 | }), 38 | }}> 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /assets/contestRules/contestRules.es.txt: -------------------------------------------------------------------------------- 1 | Quién puede participar 2 | 3 | * El programa está dirigido a los residentes de San Francisco que son elegibles para CalFresh/Medi-Cal. 4 | 5 | Cómo participar 6 | 7 | * Para participar, descargue la aplicación GRATUITA Intentional Walk. 8 | * Busque Intentional Walk en la tienda de aplicaciones. 9 | * Siga las instrucciones que aparecen en la pantalla de registro para crear un perfil. 10 | * ¡Comience a caminar! El registro de sus caminatas es opcional. La aplicación registrará automáticamente sus pasos mediante la aplicación de salud de su teléfono (Apple Health o Google Fit). 11 | * Para obtener más información, visite: iwalk.c4sf.me. 12 | 13 | Fechas del programa 14 | 15 | * Inicio: 06/01/2023, a las 12:00 a. m., hora del Pacífico. 16 | * Conclusión: 08/31/2023, a las 11:59 p. m., hora del Pacífico. 17 | 18 | Premio(s) 19 | 20 | * Al final del programa, los 10 mejores participantes recibirán un correo electrónicopara reclamar su premio. 21 | * A menos que se indique lo contrario, si el ganador no responde en el plazo de tres (3) días, otorgaremos el premio al siguiente mejor participante. 22 | * Tome en cuenta lo siguiente: La aplicación actualiza el conteo de pasos del participante cada vez que abre la aplicación. La lista de los mejores participantes se actualiza cada vez que el usuario abre la aplicación. Para registrar todos sus pasos, abra la aplicación durante el último día del concurso, el 08/31/2023. 23 | 24 | Varios 25 | 26 | * Intentional Walk está dirigido por el Departamento de Salud Pública de San Francisco. Es financiado por el Programa de Asistencia Nutricional Suplementaria (Supplemental Nutrition Assistance Program, USDA SNAP) del Departamento de Agricultura de los Estados Unidos (United States Department of Agriculture, USDA). Entre los socios comunitarios, se encuentra el Departamento de Salud Pública de California, Code for San Francisco, los San Francisco Giants, y el Departamento de Parques y Recreación de San Francisco. 27 | * El Departamento de Salud Pública de San Francisco se reserva el derecho a modificar las reglas, el programa y las fechas de premiación. 28 | * Puede encontrar la información de privacidad en la Política de privacidad. 29 | * Apple y Google no patrocinan este concurso y no son responsables de las reglas ni de los resultados. -------------------------------------------------------------------------------- /ios/IntentionalWalkAppTests/IntentionalWalkAppTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 16 | 17 | @interface IntentionalWalkAppTests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation IntentionalWalkAppTests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | #ifdef DEBUG 44 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 45 | if (level >= RCTLogLevelError) { 46 | redboxError = message; 47 | } 48 | }); 49 | #endif 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | #ifdef DEBUG 64 | RCTSetLogFunction(RCTDefaultLogFunction); 65 | #endif 66 | 67 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 68 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 69 | } 70 | 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /ios/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | platform :ios do 19 | desc "Push a new beta build to TestFlight" 20 | lane :beta do 21 | # get bundle version of app 22 | prev_version = get_info_plist_value(path: "./IntentionalWalkApp/Info.plist", key: "CFBundleVersion").to_i 23 | new_version = prev_version + 1 24 | 25 | # check if we're deploying a new version 26 | head_commit = `git rev-parse HEAD` 27 | prev_commit = `git rev-parse iOS-#{prev_version}` 28 | changelog = nil 29 | commit_after_upload = true 30 | if head_commit != prev_commit 31 | # increment build 32 | increment_build_number(xcodeproj: "IntentionalWalkApp.xcodeproj") 33 | else 34 | # adjust version numbers 35 | prev_version -= 1 36 | new_version -= 1 37 | commit_after_upload = false 38 | end 39 | # generate a changelog from previous version to head 40 | changelog = `git log --pretty=format:'%s' iOS-#{prev_version}..HEAD` 41 | changelog = changelog.gsub(/(Android|iOS) build \d+\n?/, '') 42 | changelog = "iOS build #{new_version}\n\n#{changelog}" 43 | puts "\n\n#{changelog}\n\n" 44 | build_app( 45 | workspace: "IntentionalWalkApp.xcworkspace", 46 | scheme: "IntentionalWalkApp", 47 | export_method: 'app-store', 48 | export_options: { 49 | provisioningProfiles: { 50 | "org.codeforsanfrancisco.intentionalwalk": "org.codeforsanfrancisco.intentionalwalk AppStore", 51 | } 52 | } 53 | ) 54 | upload_to_testflight(changelog: changelog, skip_submission: true) 55 | # if successful, commit build version changes and tag 56 | if commit_after_upload 57 | `git add ../..` 58 | `git commit -m "iOS build #{new_version}"` 59 | `git tag iOS-#{new_version}` 60 | `git push` 61 | `git push --tag` 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /components/multipleChoiceAnswer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'; 3 | import {Colors, GlobalStyles} from '../styles'; 4 | import {CheckBox} from 'react-native-elements'; 5 | 6 | export default function MultipleChoiceAnswer(props) { 7 | return ( 8 | props.onPress()}> 12 | props.onPress()} 26 | /> 27 | 28 | {props.text} 29 | {props.subText ? ( 30 | {props.subText} 31 | ) : ( 32 | <> 33 | )} 34 | 35 | 36 | ); 37 | } 38 | const styles = StyleSheet.create({ 39 | container: { 40 | backgroundColor: 'transparent', 41 | borderWidth: 0, 42 | padding: 0, 43 | margin: 0, 44 | }, 45 | row: { 46 | flexDirection: 'row', 47 | alignItems: 'center', 48 | justifyContent: 'flex-start', 49 | minHeight: 62, 50 | width: '100%', 51 | marginTop: 0, 52 | marginBottom: 2, 53 | paddingLeft: 14, 54 | borderRadius: GlobalStyles.rounded.borderRadius, 55 | backgroundColor: 'white', 56 | shadowColor: GlobalStyles.boxShadow.shadowColor, 57 | shadowOffset: GlobalStyles.boxShadow.shadowOffset, 58 | shadowOpacity: GlobalStyles.boxShadow.shadowOpacity, 59 | shadowRadius: GlobalStyles.boxShadow.shadowRadius, 60 | elevation: GlobalStyles.boxShadow.elevation, 61 | }, 62 | text: { 63 | fontWeight: 'bold', 64 | fontSize: 17, 65 | color: Colors.primary.purple, 66 | paddingLeft: 12, 67 | }, 68 | subText: { 69 | fontWeight: '500', 70 | fontSize: 17, 71 | color: Colors.primary.purple, 72 | paddingLeft: 12, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/notifications.js: -------------------------------------------------------------------------------- 1 | import PushNotificationIOS from '@react-native-community/push-notification-ios'; 2 | var PushNotification = require('react-native-push-notification'); 3 | 4 | PushNotification.configure({ 5 | // (optional) Called when Token is generated (iOS and Android) 6 | onRegister: function (token) { 7 | console.log('TOKEN:', token); 8 | }, 9 | 10 | // (required) Called when a remote is received or opened, or local notification is opened 11 | onNotification: function (notification) { 12 | console.log('NOTIFICATION:', notification); 13 | 14 | // process the notification 15 | 16 | // (required) Called when a remote is received or opened, or local notification is opened 17 | notification.finish(PushNotificationIOS.FetchResult.NoData); 18 | }, 19 | 20 | // IOS ONLY (optional): default: all - Permissions to register. 21 | permissions: { 22 | alert: true, 23 | badge: true, 24 | sound: true, 25 | }, 26 | 27 | // Should the initial notification be popped automatically 28 | // default: true 29 | popInitialNotification: true, 30 | 31 | /** 32 | * (optional) default: true 33 | * - Specified if permissions (ios) and token (android and ios) will requested or not, 34 | * - if not, you must call PushNotificationsHandler.requestPermissions() later 35 | * - if you are not using remote notification or do not have Firebase installed, use this: 36 | * requestPermissions: Platform.OS === 'ios' 37 | */ 38 | requestPermissions: false, 39 | }); 40 | 41 | function cancelNotification(id) { 42 | id = `${id}`; 43 | PushNotification.cancelLocalNotifications({id}); 44 | } 45 | 46 | function checkPermissions() { 47 | return new Promise((resolve, reject) => { 48 | PushNotification.checkPermissions(permissions => { 49 | resolve(permissions); 50 | }); 51 | }); 52 | } 53 | 54 | function requestPermissions() { 55 | return PushNotification.requestPermissions(); 56 | } 57 | 58 | let lastId = 0; 59 | function scheduleNotification(message, tag, date, repeatType) { 60 | lastId += 1; 61 | const id = `${lastId}`; 62 | const payload = {id, message, tag, date, userInfo: {id, tag}}; 63 | if (repeatType) { 64 | payload.repeatType = repeatType; 65 | } 66 | PushNotification.localNotificationSchedule(payload); 67 | return lastId; 68 | } 69 | 70 | export default { 71 | cancelNotification, 72 | checkPermissions, 73 | requestPermissions, 74 | scheduleNotification, 75 | }; 76 | -------------------------------------------------------------------------------- /screens/onboarding/info.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {SafeAreaView, ScrollView, StyleSheet, View, Text} from 'react-native'; 3 | import {Button, InfoBox, PaginationDots} from '../../components'; 4 | import {Colors, GlobalStyles} from '../../styles'; 5 | import {Strings} from '../../lib'; 6 | 7 | export default function InfoScreen({navigation}) { 8 | const onNextPress = () => { 9 | navigation.navigate('Permissions'); 10 | }; 11 | 12 | return ( 13 | 14 | 15 | 16 | {Strings.info.youreSignedUp} 17 | 18 | {Strings.info.fromHereText} 19 | 25 | {Strings.info.walkText} 26 | 27 | 32 | {Strings.info.recordText} 33 | 34 | 41 | {Strings.info.winText} 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | const styles = StyleSheet.create({ 55 | content: { 56 | ...GlobalStyles.content, 57 | alignItems: 'center', 58 | }, 59 | subtitle: { 60 | textAlign: 'center', 61 | marginBottom: 30, 62 | fontSize: 17, 63 | color: Colors.primary.gray2, 64 | }, 65 | info: { 66 | flex: 1, 67 | alignSelf: 'stretch', 68 | }, 69 | infoBox: { 70 | marginBottom: 30, 71 | }, 72 | button: { 73 | width: 180, 74 | }, 75 | recordButton: { 76 | marginTop: 20, 77 | width: 54, 78 | height: 54, 79 | }, 80 | starIcon: { 81 | marginTop: 20, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /lib/pedometer.android.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import GoogleFit, {Scopes} from 'react-native-google-fit'; 4 | import Realm from './realm'; 5 | import moment from 'moment'; 6 | 7 | function isAuthorized() { 8 | const options = { 9 | scopes: [Scopes.FITNESS_ACTIVITY_READ, Scopes.FITNESS_ACTIVITY_WRITE], 10 | }; 11 | return GoogleFit.authorize(options).then(authResult => { 12 | if (authResult.success) { 13 | return; 14 | } else { 15 | throw 'unauthorized'; 16 | } 17 | }); 18 | } 19 | 20 | function startUpdates(callback) { 21 | isAuthorized().then(() => { 22 | GoogleFit.observeSteps((isError, data) => 23 | getPedometerData(new Date()).then(callback), 24 | ); 25 | }); 26 | } 27 | 28 | function stopUpdates() { 29 | GoogleFit.unsubscribeListeners(); 30 | } 31 | 32 | function getPedometerData(end) { 33 | return new Promise((resolve, reject) => { 34 | Realm.getCurrentWalk().then(walk => { 35 | if (walk) { 36 | isAuthorized().then(() => { 37 | const options = { 38 | startDate: moment(walk.start).toISOString(), 39 | endDate: moment(end).toISOString(), 40 | bucketUnit: 'MINUTE', 41 | bucketInterval: 1, 42 | }; 43 | if (end.getTime() - walk.start.getTime() < 60000) { 44 | options.bucketUnit = 'SECOND'; 45 | } 46 | GoogleFit.getActivitySamples(options).then(res => { 47 | // // [ 48 | // // { 49 | // // "sourceId": "com.google.android.gms", 50 | // // "sourceName": "Android", 51 | // // "tracked": true, 52 | // // "device": "Android", 53 | // // "quantity": 18, 54 | // // "activityName": "unknown", 55 | // // "distance": 11.297628402709961, 56 | // // "end": 1584740384997, 57 | // // "start": 1584740359227 58 | // // } 59 | // // ] 60 | const data = { 61 | numberOfSteps: 0, 62 | distance: 0, 63 | }; 64 | for (let sample of res) { 65 | if (sample.quantity) { 66 | data.numberOfSteps += sample.quantity; 67 | } 68 | if (sample.distance) { 69 | data.distance += sample.distance; 70 | } 71 | } 72 | resolve(data); 73 | }); 74 | }); 75 | } 76 | }); 77 | }); 78 | } 79 | 80 | export default { 81 | startUpdates, 82 | getPedometerData, 83 | stopUpdates, 84 | }; 85 | -------------------------------------------------------------------------------- /screens/main/whereToWalk.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { 3 | Dimensions, 4 | SafeAreaView, 5 | ScrollView, 6 | StyleSheet, 7 | View, 8 | Text, 9 | Image, 10 | } from 'react-native'; 11 | import {LinkButton, PageTitle} from '../../components'; 12 | import {GlobalStyles} from '../../styles'; 13 | import {Strings} from '../../lib'; 14 | 15 | export default function WhereToWalkScreen() { 16 | const [buttonHeight, setButtonHeight] = useState(); 17 | 18 | const buttonStyle = {}; 19 | if (buttonHeight) { 20 | buttonStyle.height = buttonHeight; 21 | } 22 | 23 | const links = [ 24 | { 25 | title: Strings.whereToWalk.parksAndRecCenters, 26 | url: 'https://sfrecpark.org/facilities', 27 | }, 28 | { 29 | title: Strings.whereToWalk.hikingTrailsInSF, 30 | url: 'https://sfrecpark.org/448/Trails-Hikes', 31 | }, 32 | { 33 | title: Strings.whereToWalk.guidedWalks, 34 | url: 'https://sfrecpark.org/1226/The-EcoCenter-at-Herons-Head-Park', 35 | }, 36 | { 37 | title: Strings.whereToWalk.exerciseAndFitnessActivities, 38 | url: 'https://apm.activecommunities.com/sfrecpark/Activity_Search', 39 | }, 40 | ]; 41 | 42 | function onHeight(height) { 43 | if (height > (buttonHeight ?? 0)) { 44 | setButtonHeight(height); 45 | } 46 | } 47 | 48 | const linkBoxes = links.map((link, index) => ( 49 | 56 | )); 57 | const screenDims = Dimensions.get('screen'); 58 | const width = Math.round(screenDims.width / 3); 59 | const height = Math.round((width * 763) / 500); 60 | return ( 61 | 62 | 63 | 64 | 65 | 69 | 70 | {Strings.whereToWalk.options} 71 | 72 | {linkBoxes} 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | const styles = StyleSheet.create({ 80 | image: { 81 | marginTop: 24, 82 | marginBottom: 24, 83 | resizeMode: 'contain', 84 | alignSelf: 'center', 85 | }, 86 | options: { 87 | textAlign: 'center', 88 | marginBottom: 16, 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /components/dateNavigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View, Text, TouchableOpacity} from 'react-native'; 3 | import moment from 'moment'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import {Colors, GlobalStyles} from '../styles'; 6 | import {Strings} from '../lib'; 7 | import _ from 'lodash'; 8 | 9 | export default function DateNavigator(props) { 10 | const today = moment().startOf('day'); 11 | const yesterday = moment().startOf('day').subtract(1, 'days'); 12 | let title, prev, next; 13 | if (props.date.isSame(today)) { 14 | title = {Strings.common.today}; 15 | next = null; 16 | prev = yesterday; 17 | } else if (props.date.isSame(yesterday)) { 18 | title = {Strings.common.yesterday}; 19 | next = today; 20 | prev = moment().startOf('day').subtract(2, 'days'); 21 | } else { 22 | title = ( 23 | 24 | {_.capitalize(props.date.format('dddd'))} 25 | 26 | ); 27 | next = moment(props.date).add(1, 'day'); 28 | prev = moment(props.date).subtract(1, 'day'); 29 | } 30 | return ( 31 | 32 | props.setDate(prev)}> 35 | 36 | 37 | 38 | {title} 39 | 40 | {_.capitalize(props.date.format('MMMM D'))} 41 | 42 | 43 | 44 | {next == null ? null : ( 45 | props.setDate(next)}> 48 | 49 | 50 | )} 51 | 52 | 53 | ); 54 | } 55 | const styles = StyleSheet.create({ 56 | header: { 57 | ...GlobalStyles.rounded, 58 | ...GlobalStyles.boxShadow, 59 | backgroundColor: Colors.primary.purple, 60 | flexDirection: 'row', 61 | justifyContent: 'space-between', 62 | alignItems: 'center', 63 | height: 64, 64 | }, 65 | headerButton: { 66 | width: 64, 67 | height: 64, 68 | flexDirection: 'row', 69 | justifyContent: 'center', 70 | alignItems: 'center', 71 | }, 72 | title: { 73 | color: 'white', 74 | fontSize: 20, 75 | fontWeight: 'bold', 76 | textAlign: 'center', 77 | marginBottom: 6, 78 | }, 79 | subtitle: { 80 | color: 'white', 81 | fontSize: 12, 82 | lineHeight: 14, 83 | textAlign: 'center', 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IntentionalWalkApp", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "patch-package", 7 | "android": "react-native run-android", 8 | "ios": "react-native run-ios", 9 | "start": "react-native start", 10 | "test": "jest", 11 | "lint": "eslint .", 12 | "lint-watch": "esw -w --fix ." 13 | }, 14 | "dependencies": { 15 | "@ovalmoney/react-native-fitness": "^0.5.3", 16 | "@react-native-community/masked-view": "^0.1.11", 17 | "@react-native-community/push-notification-ios": "^1.2.2", 18 | "@react-navigation/native": "^5.9.4", 19 | "@react-navigation/stack": "^5.14.5", 20 | "@t2tx/react-native-universal-pedometer": "https://github.com/francisli/react-native-universal-pedometer#b50b4766de3bc487147d7c314fe36abc7763e730", 21 | "axios": "^0.21.1", 22 | "axios-concurrency": "^1.0.4", 23 | "lodash": "^4.17.21", 24 | "moment": "^2.24.0", 25 | "numeral": "^2.0.6", 26 | "patch-package": "^6.5.1", 27 | "react": "17.0.1", 28 | "react-moment": "^1.1.3", 29 | "react-native": "0.64.1", 30 | "react-native-autolink": "^3.0.0", 31 | "react-native-chart-kit": "^6.12.0", 32 | "react-native-device-info": "^5.5.4", 33 | "react-native-dotenv": "^3.4.8", 34 | "react-native-elements": "^1.2.7", 35 | "react-native-gesture-handler": "^1.10.3", 36 | "react-native-get-random-values": "^1.3.0", 37 | "react-native-google-fit": "^0.17.0", 38 | "react-native-keyboard-aware-scroll-view": "^0.9.4", 39 | "react-native-local-resource": "^0.1.6", 40 | "react-native-localization": "^2.3.1", 41 | "react-native-progress": "^4.0.3", 42 | "react-native-push-notification": "^3.5.2", 43 | "react-native-reanimated": "2.2.0", 44 | "react-native-safe-area-context": "^3.2.0", 45 | "react-native-screens": "3.4.0", 46 | "react-native-side-menu-updated": "^1.3.2", 47 | "react-native-splash-screen": "^3.2.0", 48 | "react-native-svg": "^13.8.0", 49 | "react-native-vector-icons": "^6.6.0", 50 | "realm": "^10.4.1", 51 | "uuid": "^7.0.2" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.12.9", 55 | "@babel/runtime": "^7.12.5", 56 | "@react-native-community/eslint-config": "^2.0.0", 57 | "babel-jest": "^26.6.3", 58 | "eslint": "^7.14.0", 59 | "eslint-watch": "^7.0.0", 60 | "jest": "^26.6.3", 61 | "metro-react-native-babel-preset": "^0.64.0", 62 | "react-test-renderer": "17.0.1" 63 | }, 64 | "overrides": { 65 | "react-native-localization": { 66 | "react-native": "$react-native" 67 | }, 68 | "react-native-local-resource": { 69 | "react-native": "$react-native" 70 | } 71 | }, 72 | "jest": { 73 | "preset": "react-native" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /components/weekNavigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View, Text, TouchableOpacity} from 'react-native'; 3 | import moment from 'moment'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import {Colors, GlobalStyles} from '../styles'; 6 | import {Strings} from '../lib'; 7 | import _ from 'lodash'; 8 | 9 | export default function WeekNavigator(props) { 10 | const startOfWeek = moment().startOf('isoweek'); 11 | const lastWeek = moment().startOf('isoweek').subtract(1, 'week'); 12 | const startOfWeekProps = moment(props.date).startOf('isoweek'); 13 | let title, prev, next; 14 | 15 | if (startOfWeekProps.isSame(startOfWeek)) { 16 | title = ( 17 | {Strings.stepGoalProgress.thisWeek} 18 | ); 19 | next = null; 20 | prev = lastWeek; 21 | } else { 22 | title = ( 23 | {Strings.stepGoalProgress.pastWeek} 24 | ); 25 | next = moment(startOfWeekProps).add(1, 'week'); 26 | prev = moment(startOfWeekProps).subtract(1, 'week'); 27 | } 28 | 29 | return ( 30 | 31 | props.setDate(prev)}> 34 | 35 | 36 | 37 | {title} 38 | 39 | {_.capitalize(moment(startOfWeekProps).format('MMM D'))} -{' '} 40 | {_.capitalize(startOfWeekProps.clone().weekday(7).format('MMM D'))} 41 | 42 | 43 | 44 | {next == null ? null : ( 45 | props.setDate(next)}> 48 | 49 | 50 | )} 51 | 52 | 53 | ); 54 | } 55 | const styles = StyleSheet.create({ 56 | header: { 57 | ...GlobalStyles.rounded, 58 | ...GlobalStyles.boxShadow, 59 | backgroundColor: Colors.primary.purple, 60 | flexDirection: 'row', 61 | justifyContent: 'space-between', 62 | alignItems: 'center', 63 | height: 64, 64 | marginBottom: 12, 65 | }, 66 | headerButton: { 67 | width: 64, 68 | height: 64, 69 | flexDirection: 'row', 70 | justifyContent: 'center', 71 | alignItems: 'center', 72 | }, 73 | title: { 74 | color: 'white', 75 | fontSize: 20, 76 | fontWeight: 'bold', 77 | textAlign: 'center', 78 | marginBottom: 6, 79 | }, 80 | subtitle: { 81 | color: 'white', 82 | fontSize: 12, 83 | lineHeight: 14, 84 | textAlign: 'center', 85 | }, 86 | }); 87 | -------------------------------------------------------------------------------- /android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:android) 17 | 18 | def get_build_gradle_version(options) 19 | File.readlines(options[:path]).each do |line| 20 | if m = line.match(/versionCode ([0-9]+)/) 21 | return m[1] 22 | end 23 | end 24 | raise Exception 25 | end 26 | 27 | def set_build_gradle_version(options) 28 | `sed -i '' -E 's/versionCode [0-9]+/versionCode #{options[:version]}/' #{options[:path]}` 29 | end 30 | 31 | platform :android do 32 | desc "Runs all the tests" 33 | lane :test do 34 | gradle(task: "test") 35 | end 36 | 37 | desc "Deploy a new Beta version to the Google Play internal track" 38 | lane :beta do 39 | # get (prev?) version of app 40 | prev_version = get_build_gradle_version(path: "../app/build.gradle").to_i 41 | new_version = prev_version + 1 42 | # check if we're deploying a new version 43 | head_commit = `git rev-parse HEAD` 44 | prev_commit = `git rev-parse And-#{prev_version}` 45 | changelog = nil 46 | commit_after_upload = true 47 | if head_commit != prev_commit 48 | # increment build 49 | set_build_gradle_version(path: "../app/build.gradle", version: new_version) 50 | else 51 | # adjust version numbers 52 | prev_version -= 1 53 | new_version -= 1 54 | commit_after_upload = false 55 | end 56 | # generate a changelog from previous version to head 57 | filename = "metadata/android/en-US/changelogs/#{new_version}.txt" 58 | `mkdir -p metadata/android/en-US/changelogs` 59 | changelog = `git log --pretty=format:'%s' And-#{prev_version}..HEAD` 60 | changelog = changelog.gsub(/(Android|iOS) build \d+\n?/, '') 61 | File.write(filename, changelog) 62 | while File.size(filename) > 500 63 | puts "Changelog greater than 500 characters- please edit #{filename} before continuing!" 64 | STDIN.getch 65 | end 66 | gradle(task: "clean assembleRelease") 67 | ENV['SUPPLY_UPLOAD_MAX_RETRIES']='5' 68 | upload_to_play_store(track: 'internal', release_status: 'draft') 69 | # if successful, commit build version changes and tag 70 | if commit_after_upload 71 | `git add ../..` 72 | `git commit -m "Android build #{new_version}"` 73 | `git tag And-#{new_version}` 74 | `git push` 75 | `git push --tag` 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /screens/main/about.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {SafeAreaView, ScrollView, StyleSheet, View} from 'react-native'; 3 | import {InfoBox, PageTitle} from '../../components'; 4 | import {Colors, GlobalStyles} from '../../styles'; 5 | import {Realm, Strings} from '../../lib'; 6 | import moment from 'moment'; 7 | 8 | export default function InfoScreen({navigation}) { 9 | const [contest, setContest] = useState(null); 10 | 11 | useEffect(() => { 12 | Realm.getContest().then(newContest => 13 | setContest(newContest ? newContest.toObject() : null), 14 | ); 15 | }, []); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | {contest && ( 23 | 24 | 30 | {Strings.formatString( 31 | Strings.about.whatText, 32 | Strings.formatString( 33 | Strings.common.range, 34 | moment(contest.start).format(Strings.common.rangeFrom), 35 | moment(contest.end).format(Strings.common.rangeTo), 36 | ), 37 | )} 38 | 39 | 45 | {Strings.formatString( 46 | Strings.about.datesText, 47 | moment(contest.start).format(Strings.common.date), 48 | Strings.formatString( 49 | Strings.common.range, 50 | moment(contest.start).format(Strings.common.rangeFrom), 51 | moment(contest.end).format(Strings.common.rangeTo), 52 | ), 53 | )} 54 | 55 | 61 | {Strings.about.prizeText} 62 | 63 | 64 | )} 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | const styles = StyleSheet.create({ 72 | title: { 73 | marginBottom: 48, 74 | }, 75 | infoBox: { 76 | marginBottom: 30, 77 | }, 78 | contest: { 79 | flex: 1, 80 | alignSelf: 'stretch', 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /screens/main/partners.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Dimensions, 4 | Image, 5 | SafeAreaView, 6 | ScrollView, 7 | StyleSheet, 8 | Text, 9 | View, 10 | } from 'react-native'; 11 | import {PageTitle} from '../../components'; 12 | import {Colors, GlobalStyles} from '../../styles'; 13 | import {Strings} from '../../lib'; 14 | 15 | export default function PartnersScreen({navigation}) { 16 | const screenDims = Dimensions.get('screen'); 17 | const width = Math.round((screenDims.width - 123) / 2); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | {Strings.partners.thanks} 26 | 27 | 28 | {Strings.partners.text} 29 | 30 | 31 | 32 | 36 | 37 | 41 | 42 | 43 | 47 | 48 | 52 | 53 | 54 | 58 | 59 | 63 | 64 | 65 | 66 | ); 67 | } 68 | 69 | const styles = StyleSheet.create({ 70 | thanks: { 71 | alignSelf: 'center', 72 | maxWidth: 240, 73 | textAlign: 'center', 74 | marginBottom: 16, 75 | }, 76 | text: { 77 | marginBottom: 25, 78 | }, 79 | row: { 80 | marginLeft: 20, 81 | marginRight: 20, 82 | marginBottom: 20, 83 | flexDirection: 'row', 84 | alignSelf: 'stretch', 85 | justifyContent: 'space-around', 86 | }, 87 | logo: { 88 | resizeMode: 'contain', 89 | alignSelf: 'center', 90 | maxHeight: 110, 91 | marginTop: 10, 92 | marginBottom: 10, 93 | }, 94 | separator: { 95 | width: 3, 96 | backgroundColor: Colors.primary.darkGreen, 97 | alignSelf: 'stretch', 98 | }, 99 | }); 100 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const axios = require('axios'); 4 | const {ConcurrencyManager} = require('axios-concurrency'); 5 | const _ = require('lodash'); 6 | 7 | import {API_BASE_URL} from '@env'; 8 | 9 | const instance = axios.create({ 10 | baseURL: `${API_BASE_URL}/api`, 11 | }); 12 | const MAX_CONCURRENT_REQUESTS = 1; 13 | // eslint-disable-next-line no-unused-vars 14 | const manager = ConcurrencyManager(instance, MAX_CONCURRENT_REQUESTS); 15 | 16 | export default { 17 | appUser: { 18 | create: function ( 19 | firstName, 20 | lastName, 21 | email, 22 | zip, 23 | age, 24 | account_id, 25 | is_latino, 26 | race, 27 | race_other, 28 | gender, 29 | gender_other, 30 | sexual_orien, 31 | sexual_orien_other, 32 | ) { 33 | return instance.post('appuser/create', { 34 | firstName, 35 | lastName, 36 | name: `${firstName} ${lastName}`, 37 | email, 38 | zip, 39 | age, 40 | account_id, 41 | is_latino, 42 | race, 43 | race_other, 44 | gender, 45 | gender_other, 46 | sexual_orien, 47 | sexual_orien_other, 48 | }); 49 | }, 50 | update: function (account_id, attributes) { 51 | const payload = _.pick(attributes, [ 52 | 'is_latino', 53 | 'race', 54 | 'race_other', 55 | 'gender', 56 | 'gender_other', 57 | 'sexual_orien', 58 | 'sexual_orien_other', 59 | ]); 60 | payload.account_id = account_id; 61 | return instance.put('appuser/create', payload); 62 | }, 63 | delete: function (account_id) { 64 | return instance.delete('appuser/delete', { 65 | data: {account_id: account_id}, 66 | }); 67 | }, 68 | }, 69 | contest: { 70 | current: function () { 71 | return instance.get('contest/current'); 72 | }, 73 | }, 74 | dailyWalk: { 75 | create: function (daily_walks, account_id) { 76 | return instance.post('dailywalk/create', { 77 | daily_walks, 78 | account_id, 79 | }); 80 | }, 81 | }, 82 | intentionalWalk: { 83 | create: function (intentional_walks, account_id) { 84 | return instance.post('intentionalwalk/create', { 85 | intentional_walks, 86 | account_id, 87 | }); 88 | }, 89 | get: function (account_id) { 90 | return instance.post('intentionalwalk/get', {account_id}); 91 | }, 92 | }, 93 | leaderboard: { 94 | get: function (device_id, contest_id) { 95 | return instance.get('leaderboard/get', { 96 | params: {device_id, contest_id}, 97 | }); 98 | }, 99 | }, 100 | weeklyGoal: { 101 | create: function (account_id, weekly_goal) { 102 | return instance.post('weeklygoal/create', { 103 | account_id, 104 | weekly_goal, 105 | }); 106 | }, 107 | get: function (account_id) { 108 | return instance.post('weeklygoal/get', {account_id}); 109 | }, 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /docs/privacy.md: -------------------------------------------------------------------------------- 1 | # [DRAFT] Intentional Walk Privacy Policy 2 | 3 | To the Intentional Walk App User, 4 | 5 | By joining San Francisco Department of Public Health (SFDPH), California Department of Public Health (CDPH), Intentional Walk program (Program) and using the Intentional Walk Application (App) you will allow SFDPH/CDPH to access the following data: total steps and walking distance during the Program, user first and last name, email address, age, and zip code of residence. Once the App is uninstalled, and/or the Program concludes, additional data will no longer be collected. SFDPH/CDPH adheres to the following regarding personal information. 6 | 1. Personally identifiable information may only be obtained through lawful means. 7 | 2. SFDPH/CDPH and Code for San Francisco does not make available, sell, or use personal data for any purpose or reason other than those specified, including third parties, except with the consent of the user, or as required by law or regulations. 8 | 3. Electronically collected personal information cannot be requested under the Public Records Act. 9 | 4. Any personal data collected shall be relevant to the purpose for which it is needed or intended. 10 | 5. Personal information is only accessible by a limited number of persons who have special access rights to such systems and are required to keep the information confidential. In addition, to maintain the safety of your personal information, SFDPH/CDPH maintains a variety of security measures when a user enters, submits, or accesses information. SFDPH/CDPH follows rules that protect the use of personal information as set forth by Federal guidelines. SFDPH/CDPH strictly follow these guidelines. 11 | 6. The App collects personal information, including user first and last name, age, zip code, email address, and password, if necessary, to establish an account on the system to connect the mobile access to the correct user record. SFDPH/CDPH uses data collected in the App for program improvement and evaluation, which also may include mobile device type, operating system version, carrier or network the device is using, and pages within the App that are visited. 12 | 7. The App will remain on the user’s device until deleted. The App may be deleted by uninstalling the mobile application. 13 | 8. The App user has the right to have any electronically collected personal information deleted by SFDPH/CDPH without reuse or distribution. Users of the App may request to have their electronically collected personal information deleted by contacting SFDPH at: intentionalwalk@sfdph.org. 14 | 10. SFDPH/CDPH will only collect walking and step-related data once the user has allowed the App access to the mobile device’s health tracker (Apple Health for iOS users, and Google Fit for Android users) that coincides with users’ personal mobile device. 15 | 11. Other parties will not collect personal information about the App user’s online activities and on different websites while using the App. 16 | 12. If you have any questions or comments about this information, the Intentional Walk Program, or App, please contact SFDPH at: intentionalwalk@sfdph.org or CDPH Privacy Office at: Privacy@cdph.ca.gov. 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /android/app/src/main/java/org/codeforsanfrancisco/intentionalwalk/MainApplication.java: -------------------------------------------------------------------------------- 1 | package org.codeforsanfrancisco.intentionalwalk; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import com.facebook.react.PackageList; 7 | import com.facebook.react.ReactApplication; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | import java.lang.reflect.InvocationTargetException; 12 | import java.util.List; 13 | 14 | import com.google.android.gms.common.GoogleApiAvailability; 15 | import com.google.android.gms.security.ProviderInstaller; 16 | import com.google.android.gms.security.ProviderInstaller.ProviderInstallListener; 17 | 18 | public class MainApplication extends Application implements ReactApplication { 19 | 20 | private final ReactNativeHost mReactNativeHost = 21 | new ReactNativeHost(this) { 22 | @Override 23 | public boolean getUseDeveloperSupport() { 24 | return BuildConfig.DEBUG; 25 | } 26 | 27 | @Override 28 | protected List getPackages() { 29 | @SuppressWarnings("UnnecessaryLocalVariable") 30 | List packages = new PackageList(this).getPackages(); 31 | // Packages that cannot be autolinked yet can be added manually here, for example: 32 | // packages.add(new MyReactNativePackage()); 33 | return packages; 34 | } 35 | 36 | @Override 37 | protected String getJSMainModuleName() { 38 | return "index"; 39 | } 40 | }; 41 | 42 | @Override 43 | public ReactNativeHost getReactNativeHost() { 44 | return mReactNativeHost; 45 | } 46 | 47 | @Override 48 | public void onCreate() { 49 | super.onCreate(); 50 | SoLoader.init(this, /* native exopackage */ false); 51 | initializeFlipper(this); // Remove this line if you don't want Flipper enabled 52 | upgradeSecurityProvider(); 53 | } 54 | 55 | /** 56 | * Loads Flipper in React Native templates. 57 | * 58 | * @param context 59 | */ 60 | private static void initializeFlipper(Context context) { 61 | if (BuildConfig.DEBUG) { 62 | try { 63 | /* 64 | We use reflection here to pick up the class that initializes Flipper, 65 | since Flipper library is not available in release mode 66 | */ 67 | Class aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper"); 68 | aClass.getMethod("initializeFlipper", Context.class).invoke(null, context); 69 | } catch (ClassNotFoundException e) { 70 | e.printStackTrace(); 71 | } catch (NoSuchMethodException e) { 72 | e.printStackTrace(); 73 | } catch (IllegalAccessException e) { 74 | e.printStackTrace(); 75 | } catch (InvocationTargetException e) { 76 | e.printStackTrace(); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Upgrade device Security Provider as needed for latest SSL compatibility. 83 | * https://gist.github.com/patrickhammond/0b13ec35160af758d98c 84 | * https://developer.android.com/training/articles/security-gms-provider 85 | */ 86 | private void upgradeSecurityProvider() { 87 | ProviderInstaller.installIfNeededAsync(this, new ProviderInstallListener() { 88 | @Override 89 | public void onProviderInstalled() { 90 | } 91 | 92 | @Override 93 | public void onProviderInstallFailed(int errorCode, Intent recoveryIntent) { 94 | GoogleApiAvailability.getInstance().showErrorNotification(MainApplication.this, errorCode); 95 | } 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 21 | 23 | 25 | 26 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /assets/privacy/privacy.en.txt: -------------------------------------------------------------------------------- 1 | To the Intentional Walk App User, 2 | 3 | Thank you for joining the San Francisco Department of Public Health (SFDPH) and California Department of Public Health (CDPH) Intentional Walk program (Program) and using the Intentional Walk Application (App). We would like to inform you that by participating in the Program, you allow SFDPH/CDPH and Code for San Francisco to access the following data: passive steps (unrecorded steps), active steps (recorded steps), total steps, and distance for recorded walks during the contest and the 30-day period prior to the contest start date, user first and last name, email address, age, zip code of residence, race, sexual orientation, and gender identity. Once the App is uninstalled, and/or the Program concludes, additional data will no longer be collected. To protect your personal information, SFDPH/CDPH adheres to the following rules and regulations: 4 | 5 | 1. Personally identifiable information may only be obtained through lawful means disclosed herein. 6 | 7 | 2. SFDPH/CDPH and Code for San Francisco does not make available, sell, or use personal data for any purpose or reason other than those specified, including third parties, except with the consent of the user, or as required by law or regulations. 8 | 9 | 3. Electronically collected personal information cannot be requested under the Public Records Act. 10 | 11 | 4. Any personal data collected shall be relevant to the purpose for which it is needed or intended. 12 | 13 | 5. Personal information is only accessible by a limited number of persons who have special access rights to such systems and are required to keep the information confidential. 14 | 15 | 6. In addition, to maintain the safety of your personal information, SFDPH/CDPH maintains security measures when a user enters, submits, or accesses information. Data is transmitted to the server using secure HTTPS encrypted protocols. 16 | 17 | 7. SFDPH/CDPH follows rules that protect the use of personal information as set forth by Federal guidelines. SFDPH/CDPH strictly follow these guidelines. 18 | 19 | 8. The App collects personal information, including user first and last name, age, zip code, email address, race, sexual orientation, and gender identity, to establish an account on the system to connect the mobile access to the correct user record. The App does not store the actual location of where users are walking. SFDPH/CDPH uses data collected in the App for program improvement and evaluation, which also may include mobile device type, operating system version, carrier or network the device is using, and pages within the App that are visited. Users can contact SFDPH to request to delete their data (contact information is below). 20 | 21 | 9. The App will remain on the user’s device until deleted. The App may be deleted by uninstalling the mobile application. 22 | 23 | 10. SFDPH/CDPH will only collect walking and step-related data once the user has allowed the App access to the mobile device’s health tracker (Apple Health for iOS users, and Google Fit for Android users). Walking and step-related data from previous users (those currently enrolled who participated in the program in previous years) will be collected and linked to their current year’s data for evaluation purposes. 24 | 25 | 11. The App user will be automatically added to the Top Walkers list (also known as the leaderboard). While the App user cannot opt out of the Top Walkers list, the App user will remain anonymous; first and last name will not be displayed. 26 | 27 | 12. Other parties will not collect personal information about the App user’s online activities and on different websites while using the App. 28 | 29 | 13. If you have any questions or comments about this information, the Intentional Walk Program, or App, please contact SFDPH at: intentionalwalk@sfdph.org or CDPH Privacy Office at: Privacy@cdph.ca.gov. 30 | 31 | 14. The App user has the right to have any electronically collected personal information deleted by the SFDPH/CDPH without reuse or distribution. Users of the App may request to have their electronically collected personal information deleted by contacting SFDPH at: intentionalwalk@sfdph.org. -------------------------------------------------------------------------------- /screens/onboarding/LoHOrigin.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {SafeAreaView, ScrollView, StyleSheet, Text, View} from 'react-native'; 3 | import { 4 | Button, 5 | MultipleChoiceQuestion, 6 | MultipleChoiceAnswer, 7 | PaginationDots, 8 | Popup, 9 | } from '../../components'; 10 | import {GlobalStyles} from '../../styles'; 11 | import {Api, Realm, Strings} from '../../lib'; 12 | 13 | export default function LoHOriginScreen({navigation, route}) { 14 | const [lohOrigin, setLohOrigin] = useState(undefined); 15 | 16 | const [isLoading, setLoading] = useState(false); 17 | 18 | const [showAlert, setShowAlert] = useState(false); 19 | const [alertTitle, setAlertTitle] = useState(''); 20 | const [alertMessage, setAlertMessage] = useState(''); 21 | 22 | const options = [ 23 | {id: 1, lohOrigin: 'YE', text: Strings.latinOrHispanicOrigin.yes}, 24 | {id: 2, lohOrigin: 'NO', text: Strings.latinOrHispanicOrigin.no}, 25 | { 26 | id: 3, 27 | lohOrigin: 'DA', 28 | text: Strings.latinOrHispanicOrigin.declineToAnswer, 29 | }, 30 | ]; 31 | 32 | function isValid() { 33 | return !isLoading && lohOrigin !== undefined; 34 | } 35 | 36 | async function onNextPress() { 37 | setLoading(true); 38 | try { 39 | // get the user object from Realm 40 | const user = await Realm.getUser(); 41 | // update the user object with the new survey value 42 | await Realm.write(() => (user.is_latino = lohOrigin)); 43 | // send the value to the server 44 | await Api.appUser.update(user.id, {is_latino: user.is_latino}); 45 | setLoading(false); 46 | navigation.navigate('WhatIsRace'); 47 | } catch { 48 | setLoading(false); 49 | setAlertTitle(Strings.common.serverErrorTitle); 50 | setAlertMessage(Strings.common.serverErrorMessage); 51 | setShowAlert(true); 52 | } 53 | } 54 | 55 | useEffect(() => { 56 | if (route?.params?.initial) { 57 | navigation.setOptions({headerLeft: null}); 58 | } 59 | }, [navigation, route]); 60 | 61 | return ( 62 | 63 | 64 | 65 | 69 | {options.map(o => ( 70 | { 75 | setLohOrigin(o.lohOrigin); 76 | }} 77 | editable={!isLoading} 78 | /> 79 | ))} 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | setShowAlert(false)}> 93 | 94 | {alertTitle} 95 | 96 | {alertMessage} 97 | 98 | 101 | 102 | 103 | 104 | ); 105 | } 106 | 107 | const styles = StyleSheet.create({ 108 | content: { 109 | ...GlobalStyles.content, 110 | alignItems: 'center', 111 | }, 112 | alertText: { 113 | textAlign: 'center', 114 | marginBottom: 48, 115 | }, 116 | button: { 117 | width: 180, 118 | }, 119 | }); 120 | -------------------------------------------------------------------------------- /components/recordedWalk.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, View, Text} from 'react-native'; 3 | import Icon from 'react-native-vector-icons/MaterialIcons'; 4 | import {GlobalStyles, Colors} from '../styles'; 5 | import {Strings} from '../lib'; 6 | import moment from 'moment'; 7 | import numeral from 'numeral'; 8 | 9 | export default function RecordedWalk(props) { 10 | let {title, date, subtitle, steps, miles, minutes} = props; 11 | const walk = props.walk; 12 | if (walk) { 13 | title = walk.timeOfWalk; 14 | const start = moment(walk.start); 15 | const today = moment().startOf('day'); 16 | const yesterday = moment(today).subtract(1, 'd'); 17 | if (start.isSameOrAfter(today)) { 18 | date = Strings.common.today; 19 | } else if (start.isSameOrAfter(yesterday)) { 20 | date = Strings.common.yesterday; 21 | } else { 22 | date = start.format('MMM D'); 23 | } 24 | steps = numeral(walk.steps).format('0,0'); 25 | miles = numeral(walk.distance * 0.000621371).format('0,0.0'); 26 | minutes = Math.round(walk.elapsedTime / 60.0); 27 | } 28 | return ( 29 | 30 | 31 | 32 | {title} 33 | {date && ( 34 | 35 | {date} 36 | 37 | )} 38 | 39 | {steps === undefined ? ( 40 | 41 | {subtitle} 42 | 43 | ) : ( 44 | <> 45 | 46 | 47 | {steps} 48 | {Strings.common.steps} 49 | 50 | 51 | {miles} 52 | {Strings.common.miles} 53 | 54 | 55 | {minutes} 56 | {Strings.common.mins} 57 | 58 | 59 | 60 | 61 | 62 | 63 | )} 64 | 65 | 66 | ); 67 | } 68 | 69 | const styles = StyleSheet.create({ 70 | container: { 71 | ...GlobalStyles.rounded, 72 | ...GlobalStyles.boxShadow, 73 | backgroundColor: 'white', 74 | height: 80, 75 | marginBottom: 16, 76 | }, 77 | clipContainer: { 78 | flex: 1, 79 | overflow: 'hidden', 80 | }, 81 | mainTitle: { 82 | color: Colors.primary.purple, 83 | fontSize: 16, 84 | fontWeight: 'bold', 85 | }, 86 | statsTitle: { 87 | color: Colors.primary.purple, 88 | fontSize: 16, 89 | }, 90 | date: { 91 | textAlign: 'right', 92 | }, 93 | subtitle: { 94 | color: Colors.primary.purple, 95 | fontSize: 12.5, 96 | }, 97 | dateContainer: { 98 | paddingLeft: 16, 99 | marginLeft: 16, 100 | marginRight: 100, 101 | borderLeftColor: Colors.primary.purple, 102 | borderLeftWidth: 1, 103 | minWidth: 90, 104 | }, 105 | row1: { 106 | flexDirection: 'row', 107 | paddingLeft: 8, 108 | paddingTop: 8, 109 | flex: 1, 110 | justifyContent: 'space-between', 111 | }, 112 | row2: { 113 | flexDirection: 'row', 114 | paddingLeft: 8, 115 | paddingTop: 4, 116 | flex: 2, 117 | justifyContent: 'space-between', 118 | }, 119 | row2Padded: { 120 | paddingRight: 100, 121 | }, 122 | stats: { 123 | justifyContent: 'center', 124 | alignItems: 'center', 125 | marginLeft: 12, 126 | }, 127 | iconContainer: { 128 | position: 'absolute', 129 | right: 8, 130 | bottom: 4, 131 | height: 60, 132 | }, 133 | icon: { 134 | color: '#BAA2C0', 135 | opacity: 0.25, 136 | }, 137 | }); 138 | -------------------------------------------------------------------------------- /screens/onboarding/permissions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Image, 4 | Linking, 5 | Platform, 6 | SafeAreaView, 7 | ScrollView, 8 | StyleSheet, 9 | Text, 10 | View, 11 | } from 'react-native'; 12 | import {Button, InfoBox, PaginationDots} from '../../components'; 13 | import {Colors, GlobalStyles} from '../../styles'; 14 | import {Fitness, Strings} from '../../lib'; 15 | 16 | export default function InfoScreen({navigation}) { 17 | const onNextPress = () => { 18 | Fitness.requestPermissions().then(permitted => { 19 | if (permitted) { 20 | navigation.navigate('MainStack', { 21 | screen: 'Home', 22 | params: {refresh: true}, 23 | }); 24 | } 25 | }); 26 | }; 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | {Strings.permissions.thingsToKnow} 34 | 35 | 36 | 37 | {Strings.permissions.takeALookText} 38 | 39 | 45 | {Strings.permissions.settingsText} 46 | 47 | 56 | {Strings.permissions.prizeText} 57 | 58 | {Platform.OS === 'android' ? ( 59 | 64 | 66 | Linking.openURL( 67 | `https://support.google.com/accounts/answer/27441?hl=${Strings.getLanguage()}`, 68 | ) 69 | }> 70 | {Strings.formatString( 71 | Strings.permissions.googleText, 72 | 73 | {Strings.permissions.getOneHere} 74 | , 75 | )} 76 | 77 | 78 | ) : null} 79 | 80 | {Platform.OS === 'android' ? ( 81 | 87 | ) : ( 88 | 91 | )} 92 | 93 | 94 | 95 | 96 | ); 97 | } 98 | 99 | const styles = StyleSheet.create({ 100 | content: { 101 | ...GlobalStyles.content, 102 | alignItems: 'center', 103 | }, 104 | subtitle: { 105 | textAlign: 'center', 106 | alignSelf: 'center', 107 | maxWidth: 250, 108 | marginBottom: 30, 109 | fontSize: 17, 110 | color: Colors.primary.gray2, 111 | }, 112 | permissions: { 113 | flex: 1, 114 | alignSelf: 'stretch', 115 | }, 116 | settingsIcon: { 117 | marginTop: 10, 118 | }, 119 | prizeIcon: { 120 | marginTop: 20, 121 | }, 122 | infoBox: { 123 | marginBottom: 30, 124 | }, 125 | infoBoxLast: { 126 | marginBottom: 30, 127 | }, 128 | linkText: { 129 | textDecorationLine: 'underline', 130 | color: Colors.primary.purple, 131 | fontWeight: 'bold', 132 | }, 133 | googleButton: { 134 | backgroundColor: Colors.primary.lightGray, 135 | resizeMode: 'contain', 136 | height: 62, 137 | }, 138 | button: { 139 | width: 180, 140 | }, 141 | }); 142 | -------------------------------------------------------------------------------- /assets/privacy/privacy.es.txt: -------------------------------------------------------------------------------- 1 | Para el usuario de la aplicación Intentional Walk: 2 | 3 | Gracias por unirse al Programa Intentional Walk (Caminata Voluntaria) (Programa) del Departamento de Salud Pública de San Francisco (San Francisco Department of Public Health, SFDPH) y Departamento de Salud Pública de California (California Department of Public Health, CDPH) y por utilizar la aplicación Intentional Walk (app). Deseamos informarle que, al participar en el Programa, permite a SFDPH/CDPH y al grupo Code for San Francisco tener acceso a los siguientes datos: pasos inactivos (pasos no registrados), pasos activos (pasos registrados), total de pasos y distancia de las caminatas registradas durante el concurso y durante el periodo de 30 días anterior a la fecha de inicio del concurso, nombre y apellido del usuario, dirección de correo electrónico, edad, código postal de residencia, raza, orientación sexual e identidad de género. Una vez que se desinstala la app o que finaliza el Programa, ya no se recopilarán datos adicionales. Para proteger su información personal, SFDPH/CDPH cumplen las siguientes normas y reglamentos: 4 | 5 | 1. La información de identificación personal solo se podrá obtener a través de los medios legales que aquí se describen. 6 | 7 | 2. Ni SFDPH/CDPH, ni Code for San Francisco ponen a disposición, venden o utilizan datos personales para ningún fin o motivo distinto del que se especifica, incluidos terceros, excepto con el consentimiento del usuario o según lo exijan las leyes o regulaciones. 8 | 9 | 3. La información personal que se recopila de forma electrónica no se puede solicitar de acuerdo con la Ley de Registros Públicos. 10 | 11 | 4. Cualquier dato personal que se recopile corresponderá al propósito para el que se necesite o esté destinado. 12 | 13 | 5. Solo un número limitado de personas, que tienen derechos especiales de acceso a los sistemas y están obligados a mantener la confidencialidad de la información, tienen acceso a la información personal. 14 | 15 | 6. Además, para mantener la seguridad de su información personal, el SFDPH/CDPH adoptan medidas de seguridad cuando un usuario introduce, envía o accede a información. Los datos se transmiten al servidor utilizando protocolos de transferencia de hipertexto seguro (Hipertext Transfer Protocol Secure, HTTPS) cifrados. 16 | 17 | 7. SFDPH/CDPH siguen las normas que protegen el uso de la información personal según lo dispuesto en las directrices federales. SFDPH/CDPH siguen estrictamente estas directrices. 18 | 19 | 8. La app recopila información personal, incluidos nombre y apellido del usuario, edad, código postal, dirección de correo electrónico, raza, orientación sexual e identidad de género, para establecer una cuenta en el sistema y conectar el acceso móvil al registro de usuario correcto. La app no almacena la ubicación real del lugar que recorren los usuarios. SFDPH/CDPH utilizan los datos que se recopilan en la app para evaluar y mejorar el programa, que incluye posiblemente el tipo de dispositivo móvil, la versión del sistema operativo, el operador o la red que utiliza el dispositivo, así como las páginas que se visitan dentro de la app. Los usuarios pueden comunicarse con el SFDPH para solicitar que se eliminen sus datos (la información de contacto se encuentra a continuación). 20 | 21 | 9. La app permanecerá en el dispositivo del usuario hasta que se elimine. La app se elimina desinstalando la aplicación móvil. 22 | 23 | 10. SFDPH/CDPH solo recopilarán los datos relacionados con las caminatas y los pasos una vez que el usuario haya permitido el acceso de la app al rastreador de salud del dispositivo móvil (Apple Health para usuarios de iOS y Google Fit para usuarios de Android). La información relacionada con las caminatas y los pasos de los usuarios anteriores (las personas que están inscritas actualmente que participaron en el programa en años pasados) se recopilará y vinculará a sus datos del año en curso para fines de evaluación. 24 | 25 | 11. El usuario de la app se añadirá automáticamente a la lista de “Top Walkers” (Mejores caminantes) (también conocida como tabla de clasificación). Aunque el usuario de la app no tiene la opción de salir de la lista de Top Walkers, permanecerá anónimo; no se mostrará ni el nombre ni el apellido. 26 | 27 | 12. Ninguna otra parte recopilará información personal de las actividades en línea del usuario de la app ni de otros sitios web mientras utiliza la app. 28 | 29 | 13. Si tiene alguna pregunta o comentario sobre esta información, sobre el Programa Intentional Walk o sobre la app, comuníquese con el SFDPH en: intentionalwalk@sfdph.org o con la Oficina de Privacidad del CDPH en: Privacy@cdph.ca.gov. 30 | 31 | 14. El usuario de la app tiene derecho a que SFDPH/CDPH eliminen cualquier información personal que se recopiló de forma electrónica sin reutilizarla ni distribuirla. Los usuarios de la app pueden solicitar que se elimine la información personal que se recopiló de forma electrónica comunicándose con el SFDPH en: intentionalwalk@sfdph.org. -------------------------------------------------------------------------------- /ios/IntentionalWalkApp/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import "RNSplashScreen.h" 9 | 10 | #ifdef FB_SONARKIT_ENABLED 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | static void InitializeFlipper(UIApplication *application) { 18 | FlipperClient *client = [FlipperClient sharedClient]; 19 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 20 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 21 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 22 | [client addPlugin:[FlipperKitReactPlugin new]]; 23 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 24 | [client start]; 25 | } 26 | #endif 27 | 28 | @implementation AppDelegate 29 | 30 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 31 | { 32 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 33 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 34 | moduleName:@"IntentionalWalkApp" 35 | initialProperties:nil]; 36 | 37 | if (@available(iOS 13.0, *)) { 38 | rootView.backgroundColor = [UIColor systemBackgroundColor]; 39 | } else { 40 | rootView.backgroundColor = [UIColor whiteColor]; 41 | } 42 | 43 | // Define UNUserNotificationCenter 44 | UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; 45 | center.delegate = self; 46 | 47 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 48 | UIViewController *rootViewController = [UIViewController new]; 49 | rootViewController.view = rootView; 50 | self.window.rootViewController = rootViewController; 51 | [self.window makeKeyAndVisible]; 52 | [RNSplashScreen show]; 53 | return YES; 54 | } 55 | 56 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 57 | { 58 | #if DEBUG 59 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 60 | #else 61 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 62 | #endif 63 | } 64 | 65 | // MARK: - Notification callbacks 66 | 67 | //Called when a notification is delivered to a foreground app. 68 | -(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler 69 | { 70 | completionHandler(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge); 71 | } 72 | 73 | // Required to register for notifications 74 | - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings 75 | { 76 | [RNCPushNotificationIOS didRegisterUserNotificationSettings:notificationSettings]; 77 | } 78 | // Required for the register event. 79 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 80 | { 81 | [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 82 | } 83 | // Required for the notification event. You must call the completion handler after handling the remote notification. 84 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo 85 | fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 86 | { 87 | [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 88 | } 89 | // Required for the registrationError event. 90 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 91 | { 92 | [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error]; 93 | } 94 | // IOS 10+ Required for localNotification event 95 | - (void)userNotificationCenter:(UNUserNotificationCenter *)center 96 | didReceiveNotificationResponse:(UNNotificationResponse *)response 97 | withCompletionHandler:(void (^)(void))completionHandler 98 | { 99 | [RNCPushNotificationIOS didReceiveNotificationResponse:response]; 100 | completionHandler(); 101 | } 102 | // IOS 4-10 Required for the localNotification event. 103 | - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification 104 | { 105 | [RNCPushNotificationIOS didReceiveLocalNotification:notification]; 106 | } 107 | 108 | @end 109 | -------------------------------------------------------------------------------- /lib/fitness.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Fitness from '@ovalmoney/react-native-fitness'; 4 | import moment from 'moment'; 5 | import {Platform, PermissionsAndroid} from 'react-native'; 6 | 7 | const permissions = [ 8 | { 9 | kind: Fitness.PermissionKinds.Steps, 10 | access: Fitness.PermissionAccesses.Read, 11 | }, 12 | { 13 | kind: Fitness.PermissionKinds.Distances, 14 | access: Fitness.PermissionAccesses.Read, 15 | }, 16 | ]; 17 | // note that Write permission is requested because on Android, Intentional Walk may be the 18 | // first app to request Step activity from Google Play Services (i.e. if Google Fit is not 19 | // installed and used)- at least, this is my best understanding at this time 20 | // on Android, we also request "Activity" permission used for the live pedometer implementation 21 | // in lib/pedometer.android.js 22 | if (Platform.OS === 'android') { 23 | permissions.push({ 24 | kind: Fitness.PermissionKinds.Steps, 25 | access: Fitness.PermissionAccesses.Write, 26 | }); 27 | permissions.push({ 28 | kind: Fitness.PermissionKinds.Distances, 29 | access: Fitness.PermissionAccesses.Write, 30 | }); 31 | permissions.push({ 32 | kind: Fitness.PermissionKinds.Activity, 33 | access: Fitness.PermissionAccesses.Read, 34 | }); 35 | permissions.push({ 36 | kind: Fitness.PermissionKinds.Activity, 37 | access: Fitness.PermissionAccesses.Write, 38 | }); 39 | } 40 | 41 | async function requestPermissions() { 42 | // on Android, we now need to separately ask for permission to read from the phone's activity sensors 43 | // SEPARATELY from asking for permissions to use Google Fit APIs below... 44 | if (Platform.OS === 'android') { 45 | const result = await PermissionsAndroid.requestMultiple([ 46 | 'android.permission.ACTIVITY_RECOGNITION', 47 | 'com.google.android.gms.permission.ACTIVITY_RECOGNITION', 48 | ]); 49 | if ( 50 | result['android.permission.ACTIVITY_RECOGNITION'] !== 51 | PermissionsAndroid.RESULTS.GRANTED && 52 | result['com.google.android.gms.permission.ACTIVITY_RECOGNITION'] !== 53 | PermissionsAndroid.RESULTS.GRANTED 54 | ) { 55 | return false; 56 | } 57 | } 58 | let permitted = await Fitness.requestPermissions(permissions); 59 | if (permitted && Platform.OS === 'android') { 60 | permitted = await Fitness.subscribeToSteps(); 61 | } 62 | return permitted; 63 | } 64 | 65 | // on android, the interval dates are not on day boundaries, so normalize 66 | function normalize(records) { 67 | const normalizedRecords = []; 68 | let day = null; 69 | for (let record of records) { 70 | if ( 71 | day == null || 72 | !( 73 | moment(record.startDate).isSameOrAfter(day.startDate) && 74 | moment(record.endDate).isBefore(day.endDate) 75 | ) 76 | ) { 77 | let startDate = moment(record.startDate).startOf('day'); 78 | let endDate = moment(startDate).add(1, 'days'); 79 | day = {startDate, endDate, quantity: record.quantity}; 80 | normalizedRecords.push(day); 81 | } else { 82 | day.quantity += record.quantity; 83 | } 84 | } 85 | return normalizedRecords; 86 | } 87 | 88 | async function getSteps(from, to) { 89 | const isAuthorized = await Fitness.isAuthorized(permissions); 90 | if (isAuthorized) { 91 | const steps = await Fitness.getSteps({ 92 | startDate: from.toISOString(), 93 | endDate: to.toISOString(), 94 | interval: 'days', 95 | }); 96 | return normalize(steps); 97 | } else { 98 | return []; 99 | } 100 | } 101 | 102 | async function getDistance(from, to) { 103 | const isAuthorized = await Fitness.isAuthorized(permissions); 104 | if (isAuthorized) { 105 | const distances = await Fitness.getDistances({ 106 | startDate: from.toISOString(), 107 | endDate: to.toISOString(), 108 | interval: 'days', 109 | }); 110 | return normalize(distances); 111 | } else { 112 | return []; 113 | } 114 | } 115 | 116 | async function getStepsAndDistances(from, to) { 117 | const [steps, distances] = await Promise.all([ 118 | getSteps(from, to), 119 | getDistance(from, to), 120 | ]); 121 | // combine steps and distances into a single payload as expected by API 122 | const dailyWalks = []; 123 | for (let [i, step] of steps.entries()) { 124 | const dailyWalk = { 125 | date: step.startDate.format('YYYY-MM-DD'), 126 | steps: step.quantity, 127 | }; 128 | if (i < distances.length && distances[i].startDate.isSame(step.startDate)) { 129 | dailyWalk.distance = distances[i].quantity; 130 | } else { 131 | // not sure if this will ever happen, but just in case steps/distances array don't match 132 | for (let distance of distances) { 133 | if (distance.startDate.isSame(step.startDate)) { 134 | dailyWalk.distance = distance.quantity; 135 | break; 136 | } 137 | } 138 | } 139 | // observed missing distance values when steps are small, set to 0 as fallback 140 | dailyWalk.distance = dailyWalk.distance || 0; 141 | dailyWalks.push(dailyWalk); 142 | } 143 | return dailyWalks; 144 | } 145 | 146 | export default { 147 | requestPermissions, 148 | getDistance, 149 | getSteps, 150 | getStepsAndDistances, 151 | }; 152 | -------------------------------------------------------------------------------- /screens/onboarding/whatIsGenderIdentity.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {SafeAreaView, ScrollView, StyleSheet, Text, View} from 'react-native'; 3 | import { 4 | Button, 5 | Input, 6 | MultipleChoiceQuestion, 7 | MultipleChoiceAnswer, 8 | PaginationDots, 9 | Popup, 10 | } from '../../components'; 11 | import {GlobalStyles, Colors} from '../../styles'; 12 | import {Api, Realm, Strings} from '../../lib'; 13 | 14 | export default function WhatIsGenderIdentityScreen({navigation, route}) { 15 | const [gender, setGender] = useState(undefined); 16 | const [genderOther, setGenderOther] = useState(''); 17 | 18 | const [isLoading, setLoading] = useState(false); 19 | 20 | const [showAlert, setShowAlert] = useState(false); 21 | const [alertTitle, setAlertTitle] = useState(''); 22 | const [alertMessage, setAlertMessage] = useState(''); 23 | 24 | const options = [ 25 | {id: 1, value: 'CF', text: Strings.whatIsYourGenderIdentity.female}, 26 | {id: 2, value: 'CM', text: Strings.whatIsYourGenderIdentity.male}, 27 | {id: 3, value: 'TF', text: Strings.whatIsYourGenderIdentity.transFemale}, 28 | {id: 4, value: 'TM', text: Strings.whatIsYourGenderIdentity.transMale}, 29 | {id: 5, value: 'NB', text: Strings.whatIsYourGenderIdentity.nonBinary}, 30 | ]; 31 | 32 | function isValid() { 33 | let filled = true; 34 | if (genderOther.trim() === '' && gender === 'OT') { 35 | filled = false; 36 | } 37 | return !isLoading && gender !== undefined && filled; 38 | } 39 | 40 | async function onNextPress() { 41 | setLoading(true); 42 | try { 43 | const user = await Realm.getUser(); 44 | await Realm.write(() => { 45 | user.gender = gender; 46 | user.gender_other = gender === 'OT' ? genderOther.trim() : ''; 47 | }); 48 | await Api.appUser.update(user.id, { 49 | gender: user.gender, 50 | gender_other: user.gender_other, 51 | }); 52 | setLoading(false); 53 | navigation.navigate('WhatIsSexualOrientation'); 54 | } catch { 55 | setLoading(false); 56 | setAlertTitle(Strings.common.serverErrorTitle); 57 | setAlertMessage(Strings.common.serverErrorMessage); 58 | setShowAlert(true); 59 | } 60 | } 61 | 62 | return ( 63 | 64 | 65 | 66 | 70 | {options.map(o => ( 71 | { 76 | setGender(o.value); 77 | }} 78 | editable={!isLoading} 79 | /> 80 | ))} 81 | { 86 | setGender('OT'); 87 | }} 88 | editable={!isLoading} 89 | /> 90 | {gender === 'OT' && ( 91 | setGenderOther(newValue)} 94 | returnKeyType="next" 95 | placeholderTextColor="#C3C3C3" 96 | editable={!isLoading} 97 | /> 98 | )} 99 | { 103 | setGender('DA'); 104 | }} 105 | editable={!isLoading} 106 | /> 107 | 108 | 109 | 115 | 116 | 117 | 118 | 119 | setShowAlert(false)}> 120 | 121 | {alertTitle} 122 | 123 | {alertMessage} 124 | 125 | 128 | 129 | 130 | 131 | ); 132 | } 133 | 134 | const styles = StyleSheet.create({ 135 | content: { 136 | ...GlobalStyles.content, 137 | alignItems: 'center', 138 | }, 139 | alertText: { 140 | textAlign: 'center', 141 | marginBottom: 48, 142 | }, 143 | button: { 144 | width: 180, 145 | }, 146 | input: { 147 | borderRadius: 4, 148 | borderWidth: 0.5, 149 | borderColor: Colors.primary.purple, 150 | marginTop: 16, 151 | marginBottom: 16, 152 | paddingLeft: 16, 153 | }, 154 | }); 155 | -------------------------------------------------------------------------------- /ios/IntentionalWalkApp.xcodeproj/xcshareddata/xcschemes/IntentionalWalkApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 90 | 92 | 98 | 99 | 100 | 101 | 107 | 109 | 115 | 116 | 117 | 118 | 120 | 121 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /screens/onboarding/whatIsSexualOrientation.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {SafeAreaView, ScrollView, StyleSheet, Text, View} from 'react-native'; 3 | import { 4 | Button, 5 | Input, 6 | MultipleChoiceQuestion, 7 | MultipleChoiceAnswer, 8 | PaginationDots, 9 | Popup, 10 | } from '../../components'; 11 | import {GlobalStyles, Colors} from '../../styles'; 12 | import {Api, Realm, Strings} from '../../lib'; 13 | 14 | export default function WhatIsSexualOrientationScreen({navigation, route}) { 15 | const [sexualOrientation, setSexualIOrientation] = useState(undefined); 16 | const [sexualOrientationOther, setSexualOrientationOther] = useState(''); 17 | 18 | const [isLoading, setLoading] = useState(false); 19 | 20 | const [showAlert, setShowAlert] = useState(false); 21 | const [alertTitle, setAlertTitle] = useState(''); 22 | const [alertMessage, setAlertMessage] = useState(''); 23 | 24 | const options = [ 25 | {id: 1, value: 'BS', text: Strings.whatIsYourSexualOrientation.bisexual}, 26 | { 27 | id: 2, 28 | value: 'SG', 29 | text: Strings.whatIsYourSexualOrientation.sameGenderLoving, 30 | }, 31 | {id: 3, value: 'QU', text: Strings.whatIsYourSexualOrientation.unsure}, 32 | { 33 | id: 4, 34 | value: 'HS', 35 | text: Strings.whatIsYourSexualOrientation.heterosexual, 36 | }, 37 | ]; 38 | 39 | function isValid() { 40 | let filled = true; 41 | if (sexualOrientationOther.trim() === '' && sexualOrientation === 'OT') { 42 | filled = false; 43 | } 44 | return !isLoading && sexualOrientation !== undefined && filled; 45 | } 46 | 47 | async function onNextPress() { 48 | setLoading(true); 49 | try { 50 | const user = await Realm.getUser(); 51 | await Realm.write(() => { 52 | user.sexual_orien = sexualOrientation; 53 | user.sexual_orien_other = 54 | sexualOrientation === 'OT' ? sexualOrientationOther.trim() : ''; 55 | }); 56 | await Api.appUser.update(user.id, { 57 | sexual_orien: user.sexual_orien, 58 | sexual_orien_other: user.sexual_orien_other, 59 | }); 60 | setLoading(false); 61 | navigation.navigate('SetYourStepGoal'); 62 | } catch { 63 | setLoading(false); 64 | setAlertTitle(Strings.common.serverErrorTitle); 65 | setAlertMessage(Strings.common.serverErrorMessage); 66 | setShowAlert(true); 67 | } 68 | } 69 | 70 | return ( 71 | 72 | 73 | 74 | 78 | {options.map(o => ( 79 | { 84 | setSexualIOrientation(o.value); 85 | }} 86 | editable={!isLoading} 87 | /> 88 | ))} 89 | { 94 | setSexualIOrientation('OT'); 95 | }} 96 | editable={!isLoading} 97 | /> 98 | {sexualOrientation === 'OT' && ( 99 | setSexualOrientationOther(newValue)} 102 | returnKeyType="next" 103 | placeholderTextColor="#C3C3C3" 104 | editable={!isLoading} 105 | /> 106 | )} 107 | { 111 | setSexualIOrientation('DA'); 112 | }} 113 | editable={!isLoading} 114 | /> 115 | 116 | 117 | 123 | 124 | 125 | 126 | 127 | setShowAlert(false)}> 128 | 129 | {alertTitle} 130 | 131 | {alertMessage} 132 | 133 | 136 | 137 | 138 | 139 | ); 140 | } 141 | 142 | const styles = StyleSheet.create({ 143 | content: { 144 | ...GlobalStyles.content, 145 | alignItems: 'center', 146 | }, 147 | alertText: { 148 | textAlign: 'center', 149 | marginBottom: 48, 150 | }, 151 | button: { 152 | width: 180, 153 | }, 154 | input: { 155 | borderRadius: 4, 156 | borderWidth: 0.5, 157 | borderColor: Colors.primary.purple, 158 | marginTop: 16, 159 | marginBottom: 16, 160 | paddingLeft: 16, 161 | }, 162 | }); 163 | -------------------------------------------------------------------------------- /routes/mainStack.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import SideMenu from 'react-native-side-menu-updated'; 3 | import {createStackNavigator} from '@react-navigation/stack'; 4 | import Icon from 'react-native-vector-icons/MaterialIcons'; 5 | import {StyleSheet, View, Text} from 'react-native'; 6 | import { 7 | HomeScreen, 8 | AboutScreen, 9 | PartnersScreen, 10 | ContestRulesScreen, 11 | PrivacyScreen, 12 | RecordedWalksScreen, 13 | TopWalkersScreen, 14 | WhereToWalkScreen, 15 | GoalProgressScreen, 16 | } from '../screens/main'; 17 | import {SetYourStepGoal} from '../screens/onboarding'; 18 | import { 19 | HamburgerButton, 20 | HamburgerMenu, 21 | Logo, 22 | Popup, 23 | Button, 24 | } from '../components'; 25 | import {Api, Realm, Strings} from '../lib'; 26 | import {Colors, GlobalStyles} from '../styles'; 27 | import {isActiveRoute, navigationRef} from '../screens/tracker'; 28 | 29 | const Stack = createStackNavigator(); 30 | 31 | export default function MainStack() { 32 | const [isMenuOpen, setIsMenuOpen] = useState(false); 33 | const [showPopupLogout, setShowPopupLogout] = useState(false); 34 | const [showPopupDelete, setShowPopupDelete] = useState(false); 35 | 36 | const logout = async () => { 37 | if (!isActiveRoute('Home')) { 38 | navigationRef.current?.navigate('Home'); 39 | } 40 | setIsMenuOpen(false); 41 | await Realm.destroyUser(); 42 | navigationRef.current?.navigate('OnboardingStack'); 43 | }; 44 | 45 | async function deleteUser() { 46 | const appUser = await Realm.getUser(); 47 | await Api.appUser.delete(appUser.id); 48 | await logout(); 49 | } 50 | 51 | return ( 52 | <> 53 | setIsMenuOpen(isOpen)} 56 | menu={ 57 | setIsMenuOpen(false)} 59 | onShowLogout={() => setShowPopupLogout(true)} 60 | onShowDeleteUser={() => setShowPopupDelete(true)} 61 | /> 62 | }> 63 | ( 67 | 72 | ), 73 | headerBackTitle: Strings.common.back.toUpperCase(), 74 | headerBackTitleVisible: true, 75 | headerTintColor: Colors.primary.purple, 76 | headerRight: props => , 77 | }}> 78 | ( 83 | setIsMenuOpen(!isMenuOpen)} /> 84 | ), 85 | }} 86 | /> 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | setShowPopupLogout(false)}> 101 | 102 | {Strings.logout.popupText} 103 | 104 | 113 | 118 | 119 | 120 | 121 | setShowPopupDelete(false)}> 124 | 125 | {Strings.deleteUser.popupText} 126 | 127 | 136 | 141 | 142 | 143 | 144 | 145 | ); 146 | } 147 | 148 | const styles = StyleSheet.create({ 149 | popupButtonsContainer: { 150 | justifyContent: 'space-between', 151 | flexDirection: 'row', 152 | flexWrap: 'wrap', 153 | width: '100%', 154 | }, 155 | popupButtons: { 156 | width: '48%', 157 | }, 158 | popupConfirmButton: { 159 | ...GlobalStyles.boxShadow, 160 | backgroundColor: Colors.primary.lightGray, 161 | }, 162 | popupConfirmText: { 163 | color: Colors.primary.purple, 164 | }, 165 | }); 166 | -------------------------------------------------------------------------------- /screens/onboarding/welcome.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import { 3 | ActivityIndicator, 4 | BackHandler, 5 | SafeAreaView, 6 | ScrollView, 7 | StyleSheet, 8 | View, 9 | Text, 10 | } from 'react-native'; 11 | import DeviceInfo from 'react-native-device-info'; 12 | import {useFocusEffect} from '@react-navigation/native'; 13 | 14 | import {ENV_NAME} from '@env'; 15 | 16 | import {Button, Popup} from '../../components'; 17 | import {Colors, GlobalStyles} from '../../styles'; 18 | import {Realm, Strings} from '../../lib'; 19 | import moment from 'moment'; 20 | 21 | export default function WelcomeScreen({navigation}) { 22 | useFocusEffect( 23 | React.useCallback(() => { 24 | const onBackPress = () => { 25 | BackHandler.exitApp(); 26 | return true; 27 | }; 28 | BackHandler.addEventListener('hardwareBackPress', onBackPress); 29 | return () => 30 | BackHandler.removeEventListener('hardwareBackPress', onBackPress); 31 | }), 32 | ); 33 | 34 | const [language, setLanguage] = useState(null); 35 | const [isLoading, setLoading] = useState(false); 36 | const [showAlert, setShowAlert] = useState(false); 37 | 38 | const selectLanguage = lang => { 39 | setLanguage(lang); 40 | moment.locale(lang); 41 | Strings.setLanguage(lang); 42 | Realm.getSettings().then(settings => 43 | Realm.write(() => (settings.lang = lang)), 44 | ); 45 | }; 46 | 47 | const continuePressed = () => { 48 | setLoading(true); 49 | Realm.updateContest() 50 | .then(contest => { 51 | setLoading(false); 52 | navigation.navigate('SignUp', {contest: contest.toObject()}); 53 | }) 54 | .catch(error => { 55 | setLoading(false); 56 | setShowAlert(true); 57 | }); 58 | }; 59 | 60 | return ( 61 | 62 | 63 | 64 | {Strings.common.welcome} 65 | {Strings.welcome.select} 66 | 73 | 80 | 87 | {language && isLoading && ( 88 | 89 | 90 | {Strings.common.pleaseWait} 91 | 92 | )} 93 | {language && !isLoading && ( 94 | 97 | )} 98 | 99 | {ENV_NAME} {DeviceInfo.getSystemName()} v{DeviceInfo.getVersion()}{' '} 100 | build {DeviceInfo.getBuildNumber()} 101 | 102 | 103 | 104 | setShowAlert(false)}> 105 | 106 | {Strings.common.serverErrorTitle} 107 | 108 | {Strings.common.serverErrorMessage} 109 | 110 | 113 | 114 | 115 | 116 | ); 117 | } 118 | 119 | const styles = StyleSheet.create({ 120 | content: { 121 | ...GlobalStyles.content, 122 | alignItems: 'center', 123 | }, 124 | subtitle: { 125 | color: Colors.primary.purple, 126 | fontSize: 18, 127 | fontWeight: 'normal', 128 | textAlign: 'center', 129 | marginBottom: 40, 130 | }, 131 | aboutText: { 132 | fontSize: 12, 133 | color: Colors.primary.gray2, 134 | textAlign: 'center', 135 | paddingLeft: 24, 136 | paddingRight: 24, 137 | paddingTop: 64, 138 | }, 139 | alertText: { 140 | textAlign: 'center', 141 | marginBottom: 48, 142 | }, 143 | button: { 144 | width: 180, 145 | }, 146 | lastButton: { 147 | marginBottom: 32, 148 | }, 149 | toggleButton: { 150 | ...GlobalStyles.rounded, 151 | backgroundColor: 'white', 152 | borderColor: Colors.primary.purple, 153 | borderWidth: 0.5, 154 | width: 180, 155 | alignItems: 'center', 156 | justifyContent: 'center', 157 | margin: 16, 158 | }, 159 | toggleButtonPressed: { 160 | backgroundColor: 'purple', 161 | borderRadius: 7, 162 | height: 50, 163 | width: 190, 164 | alignItems: 'center', 165 | justifyContent: 'center', 166 | margin: 10, 167 | }, 168 | startButton: { 169 | backgroundColor: 'purple', 170 | borderRadius: 7, 171 | height: 50, 172 | width: 190, 173 | alignItems: 'center', 174 | justifyContent: 'center', 175 | }, 176 | none: { 177 | display: 'none', 178 | }, 179 | text: { 180 | color: 'white', 181 | fontWeight: 'bold', 182 | fontSize: 24, 183 | fontFamily: 'Arial', 184 | }, 185 | loader: { 186 | flexDirection: 'row', 187 | height: 50, 188 | alignItems: 'center', 189 | }, 190 | loaderText: { 191 | color: Colors.primary.purple, 192 | fontSize: 24, 193 | fontWeight: '500', 194 | marginLeft: 10, 195 | }, 196 | }); 197 | -------------------------------------------------------------------------------- /screens/onboarding/whatIsRace.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {SafeAreaView, ScrollView, StyleSheet, Text, View} from 'react-native'; 3 | import { 4 | Button, 5 | Input, 6 | MultipleChoiceQuestion, 7 | MultipleChoiceAnswer, 8 | PaginationDots, 9 | Popup, 10 | } from '../../components'; 11 | import {GlobalStyles, Colors} from '../../styles'; 12 | import {Api, Realm, Strings} from '../../lib'; 13 | 14 | export default function WhatIsRaceScreen({navigation}) { 15 | const [raceID, setRaceID] = useState([]); 16 | const [raceOther, setRaceOther] = useState(''); 17 | 18 | const [isLoading, setLoading] = useState(false); 19 | 20 | const [showAlert, setShowAlert] = useState(false); 21 | const [alertTitle, setAlertTitle] = useState(''); 22 | const [alertMessage, setAlertMessage] = useState(''); 23 | 24 | const options = [ 25 | {id: 1, value: 'NA', text: Strings.whatIsYourRace.americanNative}, 26 | {id: 2, value: 'AS', text: Strings.whatIsYourRace.asian}, 27 | {id: 3, value: 'BL', text: Strings.whatIsYourRace.black}, 28 | {id: 4, value: 'PI', text: Strings.whatIsYourRace.pacificIsl}, 29 | {id: 5, value: 'WH', text: Strings.whatIsYourRace.white}, 30 | ]; 31 | 32 | function isValid() { 33 | let filled = true; 34 | if (raceOther.trim() === '' && raceID.indexOf(98) >= 0) { 35 | filled = false; 36 | } 37 | return !isLoading && raceID.length > 0 && filled; 38 | } 39 | 40 | async function onNextPress() { 41 | setLoading(true); 42 | 43 | const values = []; 44 | options.map(o => { 45 | if (raceID.indexOf(o.id) >= 0) { 46 | values.push(o.value); 47 | } 48 | }); 49 | if (raceID.indexOf(98) >= 0) { 50 | values.push('OT'); 51 | } 52 | if (raceID.indexOf(99) >= 0) { 53 | values.push('DA'); 54 | } 55 | 56 | try { 57 | const user = await Realm.getUser(); 58 | await Realm.write(() => { 59 | user.race = values; 60 | user.race_other = raceID.indexOf(98) >= 0 ? raceOther.trim() : ''; 61 | }); 62 | await Api.appUser.update(user.id, { 63 | race: user.race, 64 | race_other: user.race_other, 65 | }); 66 | setLoading(false); 67 | navigation.navigate('WhatIsGenderIdentity'); 68 | } catch { 69 | setLoading(false); 70 | setAlertTitle(Strings.common.serverErrorTitle); 71 | setAlertMessage(Strings.common.serverErrorMessage); 72 | setShowAlert(true); 73 | } 74 | } 75 | 76 | function pressCheck(id) { 77 | let whatsChecked = [...raceID]; 78 | const declinedID = 99; 79 | if (id === declinedID) { 80 | whatsChecked = [declinedID]; 81 | } else { 82 | if (whatsChecked.indexOf(id) >= 0) { 83 | whatsChecked.splice(whatsChecked.indexOf(id), 1); 84 | } else if (whatsChecked.indexOf(id) === -1) { 85 | whatsChecked.push(id); 86 | } 87 | if (whatsChecked.indexOf(declinedID) >= 0) { 88 | whatsChecked.splice(whatsChecked.indexOf(declinedID), 1); 89 | } 90 | } 91 | setRaceID(whatsChecked); 92 | } 93 | 94 | return ( 95 | 96 | 97 | 98 | 102 | {options.map(o => ( 103 | = 0} 107 | onPress={() => pressCheck(o.id)} 108 | editable={!isLoading} 109 | /> 110 | ))} 111 | = 0} 115 | onPress={() => pressCheck(98)} 116 | editable={!isLoading} 117 | /> 118 | {raceID.indexOf(98) >= 0 && ( 119 | { 122 | setRaceOther(newValue); 123 | }} 124 | returnKeyType="next" 125 | placeholderTextColor="#C3C3C3" 126 | editable={!isLoading} 127 | /> 128 | )} 129 | = 0} 132 | onPress={() => pressCheck(99)} 133 | editable={!isLoading} 134 | /> 135 | 136 | 137 | 143 | 144 | 145 | 146 | 147 | setShowAlert(false)}> 148 | 149 | {alertTitle} 150 | 151 | {alertMessage} 152 | 153 | 156 | 157 | 158 | 159 | ); 160 | } 161 | 162 | const styles = StyleSheet.create({ 163 | content: { 164 | ...GlobalStyles.content, 165 | alignItems: 'center', 166 | }, 167 | alertText: { 168 | textAlign: 'center', 169 | marginBottom: 48, 170 | }, 171 | button: { 172 | width: 180, 173 | }, 174 | input: { 175 | borderRadius: 4, 176 | borderWidth: 0.5, 177 | borderColor: Colors.primary.purple, 178 | marginTop: 16, 179 | marginBottom: 16, 180 | paddingLeft: 16, 181 | }, 182 | }); 183 | --------------------------------------------------------------------------------