├── .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 |
--------------------------------------------------------------------------------