├── .github
├── pull_request_template.md
└── workflows
│ ├── ci.yml
│ ├── preview.yml
│ └── production.yml
├── .gitignore
├── .husky
└── pre-push
├── .maestro
├── AI.yaml
├── Welcome.yaml
└── subflows
│ └── ExpoDevClient.yaml
├── App.js
├── GoogleService-Info.plist
├── README.md
├── app.config.ts
├── app
├── app.tsx
├── components
│ ├── AutoImage.tsx
│ ├── Avatar.tsx
│ ├── BoxShadow.tsx
│ ├── Button.tsx
│ ├── ButtonLink.tsx
│ ├── Card.tsx
│ ├── ClosedBanner.tsx
│ ├── CustomToast.tsx
│ ├── Header.tsx
│ ├── Icon.tsx
│ ├── MediaButton.tsx
│ ├── Modal.tsx
│ ├── OTAUpdates.tsx
│ ├── SafeAreaViewFixed.tsx
│ ├── Screen.tsx
│ ├── ScrollToButton.tsx
│ ├── SocialButton.tsx
│ ├── Text.tsx
│ ├── carousel
│ │ ├── Carousel.tsx
│ │ ├── CarouselCard.tsx
│ │ ├── carousel.types.ts
│ │ ├── constants.ts
│ │ └── index.ts
│ └── index.ts
├── config.ts
├── hooks
│ ├── index.ts
│ ├── useAppNavigation.ts
│ ├── useAppState.ts
│ ├── useCurrentDate.ts
│ ├── useHeader.tsx
│ └── useScrollHandlers.ts
├── i18n
│ ├── en.ts
│ ├── i18n.ts
│ ├── index.ts
│ └── translate.ts
├── navigators
│ ├── AppNavigator.tsx
│ ├── BackButton.tsx
│ ├── InfoStackNavigator.tsx
│ ├── TabNavigator.tsx
│ ├── index.ts
│ └── navigationUtilities.ts
├── screens
│ ├── ChatScreen
│ │ ├── ChatScreen.tsx
│ │ └── ai.ts
│ ├── DebugScreen.tsx
│ ├── ErrorScreen
│ │ ├── ErrorBoundary.tsx
│ │ └── ErrorDetails.tsx
│ ├── ExploreScreen
│ │ └── ExploreScreen.tsx
│ ├── InfoScreen
│ │ ├── CodeOfConductScreen.tsx
│ │ ├── ContactUsScreen.tsx
│ │ ├── CreditsScreen.tsx
│ │ ├── InfoScreen.tsx
│ │ ├── OurSponsorsScreen.tsx
│ │ └── SponsorCard.tsx
│ ├── ScheduleScreen
│ │ ├── ScheduleCard.tsx
│ │ ├── ScheduleDay.tsx
│ │ ├── ScheduleDayPicker.tsx
│ │ └── ScheduleScreen.tsx
│ ├── TalkDetailsScreen
│ │ ├── AssistantsList.tsx
│ │ ├── BreakDetailsScreen.tsx
│ │ ├── TalkDetailsHeader.tsx
│ │ ├── TalkDetailsScreen.tsx
│ │ └── WorkshopDetailsScreen.tsx
│ ├── VenuesScreen
│ │ └── VenuesScreen.tsx
│ ├── WelcomeScreen.tsx
│ └── index.ts
├── services
│ ├── api
│ │ ├── axios.ts
│ │ ├── index.ts
│ │ ├── react-query.ts
│ │ ├── webflow-api.ts
│ │ ├── webflow-api.types.ts
│ │ ├── webflow-consts.ts
│ │ ├── webflow-data.ts
│ │ └── webflow-helpers.ts
│ └── reactotron
│ │ ├── index.ts
│ │ ├── reactotron.ts
│ │ ├── reactotronClient.ts
│ │ ├── reactotronConfig.ts
│ │ └── reactotronFake.ts
├── theme
│ ├── colors.ts
│ ├── index.ts
│ ├── spacing.ts
│ ├── timing.ts
│ └── typography.ts
└── utils
│ ├── crashReporting.ts
│ ├── customSort.ts
│ ├── delay.ts
│ ├── formatDate.ts
│ ├── groupBy.ts
│ ├── ignoreWarnings.ts
│ ├── isConferencePassed.ts
│ ├── isMounted.ts
│ ├── notEmpty.ts
│ ├── openLinkInBrowser.ts
│ ├── openMap.tsx
│ ├── storage
│ ├── index.ts
│ ├── storage.test.ts
│ └── storage.ts
│ └── stringOrPlaceholder.ts
├── assets
├── branding-banner.jpg
├── download-on-app-store.svg
├── fonts
│ ├── GothamRounded-Bold.otf
│ ├── GothamRounded-Book.otf
│ ├── GothamRounded-Medium.otf
│ ├── GothamSSm-Bold.otf
│ ├── GothamSSm-Book.otf
│ └── GothamSSm-Medium.otf
├── google-play-badge.png
├── icons
│ ├── arrowDown.png
│ ├── arrowDown@2x.png
│ ├── arrowDown@3x.png
│ ├── arrowUp.png
│ ├── arrowUp@2x.png
│ ├── arrowUp@3x.png
│ ├── arrows.png
│ ├── arrows@2x.png
│ ├── arrows@3x.png
│ ├── back.png
│ ├── back@2x.png
│ ├── back@3x.png
│ ├── caretLeft.png
│ ├── caretLeft@2x.png
│ ├── caretLeft@3x.png
│ ├── caretRight.png
│ ├── caretRight@2x.png
│ ├── caretRight@3x.png
│ ├── chat.png
│ ├── chat@2x.png
│ ├── chat@3x.png
│ ├── check.png
│ ├── check@2x.png
│ ├── check@3x.png
│ ├── explore.png
│ ├── explore@2x.png
│ ├── explore@3x.png
│ ├── github.png
│ ├── github@2x.png
│ ├── github@3x.png
│ ├── info.png
│ ├── info@2x.png
│ ├── info@3x.png
│ ├── ladybug.png
│ ├── ladybug@2x.png
│ ├── ladybug@3x.png
│ ├── link.png
│ ├── link@2x.png
│ ├── link@3x.png
│ ├── schedule.png
│ ├── schedule@2x.png
│ ├── schedule@3x.png
│ ├── twitter.png
│ ├── twitter@2x.png
│ ├── twitter@3x.png
│ ├── venue.png
│ ├── venue@2x.png
│ ├── venue@3x.png
│ ├── youtube.png
│ ├── youtube@2x.png
│ └── youtube@3x.png
└── images
│ ├── app-icon-all.png
│ ├── app-icon-android-adaptive-background.png
│ ├── app-icon-android-adaptive-foreground.png
│ ├── app-icon-android-legacy.png
│ ├── app-icon-ios.png
│ ├── app-icon-web-favicon.png
│ ├── card-offset.png
│ ├── card-offset@2x.png
│ ├── card-offset@3x.png
│ ├── cr-logo.png
│ ├── cr-logo@2x.png
│ ├── cr-logo@3x.png
│ ├── info-conf.jpg
│ ├── info-conf@2x.jpg
│ ├── info-conf@3x.jpg
│ ├── info-ir1@2x.png
│ ├── info-ir1@3x.png
│ ├── info-ir2.png
│ ├── info-ir2@2x.png
│ ├── info-ir2@3x.png
│ ├── info-ir3.png
│ ├── info-ir3@2x.png
│ ├── info-ir3@3x.png
│ ├── info-r1.png
│ ├── sad-face.png
│ ├── sad-face@2x.png
│ ├── sad-face@3x.png
│ ├── splash-logo-all.png
│ ├── splash-logo-android-universal.png
│ ├── splash-logo-ios-mobile.png
│ ├── splash-logo-ios-tablet.png
│ ├── splash-logo-web.png
│ ├── talk-curve.png
│ ├── talk-curve@2x.png
│ ├── talk-curve@3x.png
│ ├── talk-shape.png
│ ├── talk-shape@2x.png
│ ├── talk-shape@3x.png
│ ├── testdouble-breaks.png
│ ├── testdouble-breaks@2x.png
│ ├── testdouble-breaks@3x.png
│ ├── welcome-shapes.png
│ ├── welcome-shapes@2x.png
│ ├── welcome-shapes@3x.png
│ ├── workshop-curve.png
│ ├── workshop-curve@2x.png
│ ├── workshop-curve@3x.png
│ ├── workshop-shape.png
│ ├── workshop-shape@2x.png
│ └── workshop-shape@3x.png
├── babel.config.js
├── bin
└── postInstall
├── eas.json
├── google-services.json
├── ignite
└── templates
│ ├── component
│ └── NAME.tsx.ejs
│ ├── navigator
│ └── NAMENavigator.tsx.ejs
│ └── screen
│ └── NAMEScreen.tsx.ejs
├── jest.config.js
├── metro.config.js
├── package.json
├── react-native.config.js
├── test
├── i18n.test.ts
├── mockFile.ts
└── setup.ts
├── tsconfig.json
└── yarn.lock
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 |
4 |
5 | [Trello Card]()
6 |
7 | Summary of the changes and any helpful notes for reviewers and testers.
8 |
9 | # Graphics
10 |
11 | | iOS | Android |
12 | | -------------- | ------------------ |
13 | | ios_screenshot | android_screenshot |
14 |
15 | # Checklist:
16 |
17 | - [ ] I have done a thorough self-review of my code
18 | - [ ] I have tested with a screen reader and font-scaling turned on and added necessary accessibility features
19 | - [ ] I have run tests and linter
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI — Yarn test and lint
2 |
3 | on: [workflow_dispatch, push]
4 |
5 | jobs:
6 | build-test-and-lint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Clone repository
10 | uses: actions/checkout@v3
11 |
12 | - name: Use Node.js 16
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version: 16.x
16 | cache: yarn
17 |
18 | - name: Install dependencies
19 | run: yarn install
20 |
21 | - name: Run tests
22 | run: yarn test
23 |
24 | - name: Run linter
25 | run: yarn lint
26 |
--------------------------------------------------------------------------------
/.github/workflows/preview.yml:
--------------------------------------------------------------------------------
1 | name: Preview
2 |
3 | on: [workflow_dispatch]
4 |
5 | jobs:
6 | preview:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Clone repository
10 | uses: actions/checkout@v3
11 |
12 | - name: Use Node.js 16
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version: 16.x
16 | cache: yarn
17 |
18 | - name: Install dependencies
19 | run: yarn install
20 |
21 | - name: Run tests
22 | run: yarn test
23 |
24 | - name: Run linter
25 | run: yarn lint
26 |
27 | - name: Setup Expo and EAS
28 | uses: expo/expo-github-action@v7
29 | with:
30 | expo-version: 5.x
31 | eas-version: latest
32 | token: ${{ secrets.EXPO_TOKEN }}
33 |
34 | - name: Build on EAS
35 | run: eas build --platform all --profile preview:device --non-interactive --clear-cache
36 |
--------------------------------------------------------------------------------
/.github/workflows/production.yml:
--------------------------------------------------------------------------------
1 | name: Production build when pushing to main
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | production:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Clone repository
13 | uses: actions/checkout@v3
14 |
15 | - name: Use Node.js 16
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: 16.x
19 | cache: yarn
20 |
21 | - name: Install dependencies
22 | run: yarn install
23 |
24 | - name: Run tests
25 | run: yarn test
26 |
27 | - name: Run linter
28 | run: yarn lint
29 |
30 | - name: Setup Expo and EAS
31 | uses: expo/expo-github-action@v7
32 | with:
33 | expo-version: 5.x
34 | eas-version: latest
35 | token: ${{ secrets.EXPO_TOKEN }}
36 |
37 | - name: Build on EAS
38 | if: ${{ github.ref == 'refs/heads/main' }}
39 | run: eas build --platform all --profile production --non-interactive --clear-cache
40 |
--------------------------------------------------------------------------------
/.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 | *.apk
23 | *.aab
24 | *.xcuserstate
25 | ios/.xcode.env.local
26 | project.xcworkspace
27 |
28 | # Android/IntelliJ
29 | #
30 | build/
31 | .idea
32 | .gradle
33 | local.properties
34 | *.iml
35 | *.hprof
36 | *.keystore
37 | !debug.keystore
38 |
39 | # node.js
40 | #
41 | node_modules/
42 | npm-debug.log
43 | yarn-error.log
44 |
45 |
46 | # fastlane
47 | #
48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
49 | # screenshots whenever they are needed.
50 | # For more information about the recommended setup visit:
51 | # https://docs.fastlane.tools/best-practices/source-control/
52 |
53 | **/fastlane/report.xml
54 | **/fastlane/Preview.html
55 | **/fastlane/screenshots
56 | **/fastlane/test_output
57 |
58 | # Bundle artifact
59 | *.jsbundle
60 |
61 | # CocoaPods
62 | /ios/Pods/
63 |
64 | # Ignite-specific items below
65 | # You can safely replace everything above this comment with whatever is
66 | # in the default .gitignore generated by React-Native CLI
67 |
68 | # VS Code
69 | .vscode
70 |
71 | # Expo
72 | .expo/*
73 | bin/Exponent.app
74 |
75 | npm-debug.*
76 | *.jks
77 | *.p8
78 | *.p12
79 | *.key
80 | *.mobileprovision
81 | *.orig.*
82 | web-build/
83 |
84 | ios/
85 | android/
86 |
87 | # Configurations
88 | !env.js
89 |
90 | # @generated expo-cli sync-e7dcf75f4e856f7b6f3239b3f3a7dd614ee755a8
91 | # The following patterns were generated by expo-cli
92 |
93 | # Expo
94 | dist/
95 |
96 | # @end expo-cli
97 |
98 | # Temporary files created by Metro to check the health of the file watcher
99 | .metro-health-check*
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | yarn lint-warn
5 | yarn test
--------------------------------------------------------------------------------
/.maestro/AI.yaml:
--------------------------------------------------------------------------------
1 | # flow: Test the AI Chat feature
2 |
3 | appId: infinitered.stage.ChainReactConf
4 | ---
5 | - runFlow: subflows/ExpoDevClient.yaml
6 | - tapOn: "AI Chat, tab, 5 of 5"
7 | - tapOn:
8 | id: "aiChatInput"
9 | - "eraseText"
10 | - inputText: "Who are the speakers from Meta?"
11 | - tapOn: "Send"
12 | - assertVisible: "Riccardo Cipolleschi and Christoph Purrer are the speakers from Meta.*"
13 | - inputText: "When are their talks?"
14 | - tapOn: "Send"
15 | - assertVisible: "Riccardo Cipolleschi's talk is on Thursday, May 18 at 2:30 pm. Christoph Purrer's talk is on Friday, May 19 at 10:30 am.*"
16 | - inputText: "Where is the conference?"
17 | - tapOn: "Send"
18 | - assertVisible: "The conference is being held at The Gerding Theater at The Armory in Portland, Oregon.*"
19 | - inputText: "What are some good places to eat near there?"
20 | - tapOn: "Send"
21 | - assertVisible: "There are many great places to eat near The Armory, including Tasty n Alder, Lardo, and Blue Star Donuts.*"
22 | - tapOn:
23 | point: "50%,35%"
24 |
--------------------------------------------------------------------------------
/.maestro/Welcome.yaml:
--------------------------------------------------------------------------------
1 | # flow: Test the Welcome Screen appears and transitions to schedule
2 |
3 | appId: infinitered.stage.ChainReactConf
4 | ---
5 | - runFlow: subflows/ExpoDevClient.yaml
6 | - assertVisible: "Welcome to Chain React.*"
7 | - tapOn: "See the schedule"
8 | - assertVisible: "Schedule"
9 | - assertVisible: "Wed, May 17"
10 |
--------------------------------------------------------------------------------
/.maestro/subflows/ExpoDevClient.yaml:
--------------------------------------------------------------------------------
1 | # flow: ExpoDevClient
2 | # intent:
3 | # Open up Expo's dev client menu and connect to
4 | # the locally running dev server, getting to the Welcome screen
5 |
6 | appId: infinitered.stage.ChainReactConf
7 | ---
8 | - launchApp:
9 | clearState: true # Will clear our saved navigation state, we may not always want this
10 | - tapOn: "Enter URL manually"
11 | - inputText: "http://localhost:8081/?disableOnboarding=1"
12 | - tapOn:
13 | point: "50%,50%"
14 | - tapOn: "Connect"
15 | - assertVisible: "Welcome to Chain React.*"
16 | - tapOn: "See the schedule"
17 | - assertVisible: "Wed, May 17"
18 | #
19 | # alternative method
20 | # use deep link and press open?
21 | # exp+chainreactapp2023://expo-development-client/?url=http%3A%2F%2F192.168.5.29%3A8081
22 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | // This is the entry point if you run `yarn expo:start`
2 | // If you run `yarn ios` or `yarn android`, it'll use ./index.js instead.
3 | import App from "./app/app.tsx"
4 | import React from "react"
5 | import { registerRootComponent } from "expo"
6 | import * as SplashScreen from "expo-splash-screen"
7 | import messaging from "@react-native-firebase/messaging"
8 |
9 | // Register background handler
10 | messaging().setBackgroundMessageHandler(async (_remoteMessage) => {
11 | // console.tron.log('Message handled in the background!', remoteMessage);
12 | })
13 |
14 | SplashScreen.preventAutoHideAsync()
15 |
16 | function IgniteApp() {
17 | return
18 | }
19 |
20 | registerRootComponent(IgniteApp)
21 | export default IgniteApp
22 |
--------------------------------------------------------------------------------
/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CLIENT_ID
6 | 1051103944258-kiu02m337mppqd2p8o71boood0gjs3f2.apps.googleusercontent.com
7 | REVERSED_CLIENT_ID
8 | com.googleusercontent.apps.1051103944258-kiu02m337mppqd2p8o71boood0gjs3f2
9 | API_KEY
10 | AIzaSyAYh9bdhPS3h2OY0j5qxvteslvaaRyPrN0
11 | GCM_SENDER_ID
12 | 1051103944258
13 | PLIST_VERSION
14 | 1
15 | BUNDLE_ID
16 | infinitered.stage.ChainReactConf
17 | PROJECT_ID
18 | chainreactapp2023
19 | STORAGE_BUCKET
20 | chainreactapp2023.appspot.com
21 | IS_ADS_ENABLED
22 |
23 | IS_ANALYTICS_ENABLED
24 |
25 | IS_APPINVITE_ENABLED
26 |
27 | IS_GCM_ENABLED
28 |
29 | IS_SIGNIN_ENABLED
30 |
31 | GOOGLE_APP_ID
32 | 1:1051103944258:ios:2f134a7cc5f68cb3a157db
33 |
34 |
--------------------------------------------------------------------------------
/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ExpoConfig, ConfigContext } from "@expo/config"
2 | import { version } from "./package.json"
3 |
4 | const BUILD_NUMBER = 9
5 |
6 | export default ({ config }: ConfigContext): ExpoConfig => ({
7 | ...config,
8 | name: "Chain React App 2023",
9 | slug: "ChainReactApp2023",
10 | scheme: "chainreactapp",
11 | version,
12 | orientation: "portrait",
13 | icon: "./assets/images/app-icon-all.png",
14 | splash: {
15 | image: "./assets/images/splash-logo-all.png",
16 | resizeMode: "contain",
17 | backgroundColor: "#081828",
18 | },
19 | updates: {
20 | checkAutomatically: "ON_ERROR_RECOVERY",
21 | enabled: true,
22 | fallbackToCacheTimeout: 0,
23 | url: "https://u.expo.dev/b72c79d7-7c87-4aa7-b964-998dcff69e07",
24 | },
25 | runtimeVersion: {
26 | policy: "sdkVersion",
27 | },
28 | jsEngine: "hermes",
29 | assetBundlePatterns: ["**/*"],
30 | android: {
31 | icon: "./assets/images/app-icon-android-legacy.png",
32 | package: "com.chainreactapp",
33 | versionCode: BUILD_NUMBER,
34 | adaptiveIcon: {
35 | foregroundImage: "./assets/images/app-icon-android-adaptive-foreground.png",
36 | backgroundImage: "./assets/images/app-icon-android-adaptive-background.png",
37 | },
38 | splash: {
39 | image: "./assets/images/splash-logo-android-universal.png",
40 | resizeMode: "contain",
41 | backgroundColor: "#081828",
42 | },
43 | googleServicesFile: `./google-services.json`,
44 | intentFilters: [
45 | {
46 | action: "VIEW",
47 | data: { scheme: "https" },
48 | },
49 | {
50 | action: "VIEW",
51 | data: { scheme: "google.navigation" },
52 | },
53 | {
54 | action: "VIEW",
55 | data: { scheme: "geo" },
56 | },
57 | {
58 | action: "SEND",
59 | data: { scheme: "mailto" },
60 | },
61 | ],
62 | },
63 | ios: {
64 | icon: "./assets/images/app-icon-ios.png",
65 | supportsTablet: false,
66 | bundleIdentifier: "infinitered.stage.ChainReactConf",
67 | buildNumber: String(BUILD_NUMBER),
68 | splash: {
69 | image: "./assets/images/splash-logo-ios-mobile.png",
70 | tabletImage: "./assets/images/splash-logo-ios-tablet.png",
71 | resizeMode: "contain",
72 | backgroundColor: "#081828",
73 | },
74 | googleServicesFile: `./GoogleService-Info.plist`,
75 | infoPlist: {
76 | UIBackgroundModes: ["fetch", "remote-notification"],
77 | UIStatusBarHidden: true,
78 | },
79 | },
80 | web: {
81 | favicon: "./assets/images/app-icon-web-favicon.png",
82 | splash: {
83 | image: "./assets/images/splash-logo-web.png",
84 | resizeMode: "contain",
85 | backgroundColor: "#081828",
86 | },
87 | },
88 | owner: "infinitered",
89 | extra: {
90 | eas: {
91 | projectId: "b72c79d7-7c87-4aa7-b964-998dcff69e07",
92 | },
93 | },
94 | plugins: [
95 | "@react-native-firebase/app",
96 | "@react-native-firebase/crashlytics",
97 | ["expo-build-properties", { ios: { useFrameworks: "static" } }],
98 | ["expo-updates", { username: "infinitered" }],
99 | ["expo-localization"],
100 | [
101 | "expo-build-properties",
102 | {
103 | android: {
104 | enableProguardInReleaseBuilds: true,
105 | enableShrinkResourcesInReleaseBuilds: true,
106 | },
107 | },
108 | ],
109 | ],
110 | })
111 |
--------------------------------------------------------------------------------
/app/app.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to the main entry point of the app. In this file, we'll
3 | * be kicking off our app.
4 | *
5 | * The app navigation resides in ./app/navigators, so head over there
6 | * if you're interested in adding screens and navigators.
7 | */
8 | import "./i18n"
9 | import "./utils/ignoreWarnings"
10 | import { useFonts } from "expo-font"
11 | import React, { useLayoutEffect } from "react"
12 | import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context"
13 | import { AppNavigator, useNavigationPersistence } from "./navigators"
14 | import { ErrorBoundary } from "./screens/ErrorScreen/ErrorBoundary"
15 | import * as storage from "./utils/storage"
16 | import { customFontsToLoad } from "./theme"
17 | import { setupReactotron } from "./services/reactotron/reactotron"
18 | import Config from "./config"
19 | import { QueryClientProvider } from "@tanstack/react-query"
20 | import { queryClient } from "./services/api/react-query"
21 | import { CustomToast, OTAUpdates } from "./components"
22 |
23 | // Set up Reactotron, which is a free desktop app for inspecting and debugging
24 | // React Native apps. Learn more here: https://github.com/infinitered/reactotron
25 | setupReactotron({
26 | // clear the Reactotron window when the app loads/reloads
27 | clearOnLoad: true,
28 | // generally going to be localhost
29 | host: "localhost",
30 | // Reactotron can monitor AsyncStorage for you
31 | useAsyncStorage: true,
32 | // log the initial restored state from AsyncStorage
33 | logInitialState: true,
34 | // log out any snapshots as they happen (this is useful for debugging but slow)
35 | logSnapshots: false,
36 | })
37 |
38 | export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE"
39 |
40 | interface AppProps {
41 | hideSplashScreen: () => Promise
42 | }
43 |
44 | /**
45 | * This is the root component of our app.
46 | */
47 | function App(props: AppProps) {
48 | const { hideSplashScreen } = props
49 | const {
50 | initialNavigationState,
51 | onNavigationStateChange,
52 | isRestored: isNavigationStateRestored,
53 | } = useNavigationPersistence(storage, NAVIGATION_PERSISTENCE_KEY)
54 | const [recoveredFromError, setRecoveredFromError] = React.useState(false)
55 | const [areFontsLoaded] = useFonts(customFontsToLoad)
56 |
57 | useLayoutEffect(() => {
58 | // hide splash screen after 500ms
59 | setTimeout(hideSplashScreen, 500)
60 | })
61 |
62 | useLayoutEffect(() => {
63 | if (recoveredFromError) {
64 | setRecoveredFromError(false)
65 | }
66 | })
67 |
68 | // Before we show the app, we have to wait for our state to be ready.
69 | // In the meantime, don't render anything. This will be the background
70 | // color set in native by rootView's background color.
71 | // In iOS: application:didFinishLaunchingWithOptions:
72 | // In Android: https://stackoverflow.com/a/45838109/204044
73 | // You can replace with your own loading component if you wish.
74 | if (!isNavigationStateRestored || !areFontsLoaded) return null
75 |
76 | // otherwise, we're ready to render the app
77 | return (
78 |
79 | setRecoveredFromError(true)}>
80 |
81 |
85 |
86 |
87 |
88 |
89 |
90 | )
91 | }
92 |
93 | export default App
94 |
--------------------------------------------------------------------------------
/app/components/AutoImage.tsx:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect, useState } from "react"
2 | import {
3 | Image,
4 | ImageProps,
5 | ImageSourcePropType,
6 | ImageStyle,
7 | ImageURISource,
8 | Platform,
9 | } from "react-native"
10 |
11 | /**
12 | * Get the dimensions of an image, scaled to fit the specified width
13 | * while maintaining the image's original ratio.
14 | *
15 | * @param image ImageSourcePropType—can be a remote image or a local image
16 | * @param width number—width of the space you want image to fill
17 | * @returns { height: number, width: number } as ImageStyle
18 | */
19 | export const getImageDimensionsForWidth = (image: ImageSourcePropType, width: number) => {
20 | const { height: rawImageHeight, width: rawImageWidth } = Image.resolveAssetSource(image)
21 | const imageRatio = width / rawImageWidth
22 | return {
23 | height: rawImageHeight * imageRatio,
24 | width,
25 | } as ImageStyle
26 | }
27 |
28 | // TODO: document new props
29 | export interface AutoImageProps extends ImageProps {
30 | /**
31 | * How wide should the image be?
32 | */
33 | maxWidth?: number
34 | /**
35 | * How tall should the image be?
36 | */
37 | maxHeight?: number
38 | }
39 |
40 | /**
41 | * A hook that will return the scaled dimensions of an image based on the
42 | * provided dimesions' aspect ratio. If no desired dimensions are provided,
43 | * it will return the original dimensions of the remote image.
44 | *
45 | * How is this different from `resizeMode: 'contain'`? Firstly, you can
46 | * specify only one side's size (not both). Secondly, the image will scale to fit
47 | * the desired dimensions instead of just being contained within its image-container.
48 | *
49 | */
50 | export function useAutoImage(
51 | remoteUri: string,
52 | dimensions?: [maxWidth?: number, maxHeight?: number],
53 | ): [width: number, height: number] {
54 | const [[remoteWidth, remoteHeight], setRemoteImageDimensions] = useState([0, 0])
55 | const remoteAspectRatio = remoteWidth / remoteHeight
56 | const [maxWidth, maxHeight] = dimensions ?? []
57 |
58 | useLayoutEffect(() => {
59 | if (!remoteUri) return
60 |
61 | Image.getSize(remoteUri, (w, h) => setRemoteImageDimensions([w, h]))
62 | }, [remoteUri])
63 |
64 | if (Number.isNaN(remoteAspectRatio)) return [0, 0]
65 |
66 | if (maxWidth && maxHeight) {
67 | const aspectRatio = Math.min(maxWidth / remoteWidth, maxHeight / remoteHeight)
68 | return [remoteWidth * aspectRatio, remoteHeight * aspectRatio]
69 | } else if (maxWidth) {
70 | return [maxWidth, maxWidth / remoteAspectRatio]
71 | } else if (maxHeight) {
72 | return [maxHeight * remoteAspectRatio, maxHeight]
73 | } else {
74 | return [remoteWidth, remoteHeight]
75 | }
76 | }
77 |
78 | /**
79 | * An Image component that automatically sizes a remote or data-uri image.
80 | *
81 | * - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-AutoImage.md)
82 | */
83 | export function AutoImage(props: AutoImageProps) {
84 | const { maxWidth, maxHeight, ...ImageProps } = props
85 | const source = props.source as ImageURISource
86 |
87 | const [width, height] = useAutoImage(
88 | Platform.select({
89 | web: (source?.uri as string) ?? (source as string),
90 | default: source?.uri as string,
91 | }),
92 | [maxWidth, maxHeight],
93 | )
94 |
95 | return
96 | }
97 |
--------------------------------------------------------------------------------
/app/components/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Image, StyleProp, View, ViewStyle, ImageStyle, ImageSourcePropType } from "react-native"
3 | import { spacing } from "../theme"
4 |
5 | export type AvatarPresets = keyof typeof $viewPresets
6 |
7 | export type AvatarProps = {
8 | /**
9 | * An optional style override useful for padding & margin.
10 | */
11 | style?: StyleProp
12 | /**
13 | * An optional style to override the Image
14 | */
15 | imageStyle?: StyleProp
16 | /**
17 | * Multiple avatars to display
18 | */
19 | sources: ImageSourcePropType[]
20 | /**
21 | * Preset for the avatar
22 | */
23 | preset?: AvatarPresets
24 | }
25 |
26 | /**
27 | * Displays an avatar for a workshop, talk or speaker panel
28 | */
29 | export const Avatar: React.FC = (props) => {
30 | const { style: $styleOverride, imageStyle } = props
31 | const preset: AvatarPresets =
32 | props.sources.length > 1
33 | ? "multi-speaker"
34 | : props.preset && $viewPresets[props.preset]
35 | ? props.preset
36 | : "workshop"
37 | const $imageStyle = Object.assign({}, $viewPresets[preset], imageStyle)
38 | const $containerStyle = [$baseContainerStyle, $styleOverride]
39 |
40 | return (
41 |
42 | {props.sources.map((source, index) => (
43 |
49 | ))}
50 |
51 | )
52 | }
53 |
54 | const $baseContainerStyle: ViewStyle = {
55 | flex: 1,
56 | flexDirection: "row",
57 | }
58 |
59 | const $viewPresets = {
60 | workshop: {
61 | width: 80,
62 | height: 80,
63 | borderRadius: 40,
64 | } as StyleProp,
65 |
66 | talk: {
67 | width: 100,
68 | height: 100,
69 | borderRadius: 50,
70 | } as StyleProp,
71 |
72 | "multi-speaker": {
73 | width: 42,
74 | height: 42,
75 | borderRadius: 21,
76 | } as StyleProp,
77 |
78 | party: {} as StyleProp,
79 |
80 | recurring: {} as StyleProp,
81 | }
82 |
83 | const $panelImageStyle: ImageStyle = {
84 | marginTop: spacing.small,
85 | marginStart: -spacing.small,
86 | }
87 |
--------------------------------------------------------------------------------
/app/components/BoxShadow.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Image, ImageStyle, StyleProp, View, ViewStyle } from "react-native"
3 | import { colors, spacing } from "../theme"
4 |
5 | const cardOffset = require("../../assets/images/card-offset.png")
6 |
7 | type Presets = keyof typeof $offsetPresets
8 |
9 | interface BoxShadowProps {
10 | /**
11 | * The children node(s) to build the box under
12 | */
13 | children: React.ReactNode
14 | /**
15 | * One of the different types of button presets.
16 | */
17 | preset?: Presets
18 | /**
19 | * Style override for the outside container
20 | */
21 | style?: StyleProp
22 | /**
23 | * Offset spacing amount the shadow should be from the children
24 | */
25 | offset?: number
26 | }
27 |
28 | export function BoxShadow(props: BoxShadowProps): React.ReactElement {
29 | const [height, setHeight] = React.useState(undefined)
30 | const [width, setWidth] = React.useState(undefined)
31 | const preset: Presets = props.preset ?? "default"
32 | const $offset: StyleProp = [$offsetPresets[preset], { height, width }]
33 | const offsetAmount = props.offset || spacing.tiny
34 | const $offsetContainerSpacing = { left: offsetAmount, top: offsetAmount }
35 |
36 | return (
37 |
38 | {
40 | setHeight(e.nativeEvent.layout.height)
41 | setWidth(e.nativeEvent.layout.width)
42 | }}
43 | style={{ marginEnd: offsetAmount, marginBottom: offsetAmount }}
44 | >
45 |
46 |
47 |
48 | {props.children}
49 |
50 |
51 | )
52 | }
53 |
54 | const $offsetContainer: ViewStyle = {
55 | position: "absolute",
56 | }
57 |
58 | const $offsetPresets = {
59 | default: [
60 | {
61 | borderColor: colors.palette.primary500,
62 | borderWidth: 1,
63 | tintColor: colors.palette.primary500,
64 | },
65 | ] as StyleProp,
66 |
67 | primary: [
68 | {
69 | backgroundColor: colors.palette.primary500,
70 | tintColor: colors.palette.secondary500,
71 | borderColor: colors.palette.neutral700,
72 | borderWidth: 1,
73 | },
74 | ] as StyleProp,
75 |
76 | secondary: [
77 | {
78 | tintColor: colors.palette.secondary500,
79 | borderColor: colors.palette.neutral400,
80 | borderWidth: 1,
81 | },
82 | ] as StyleProp,
83 |
84 | reversed: [
85 | {
86 | borderColor: colors.palette.neutral400,
87 | borderWidth: 1,
88 | tintColor: colors.palette.neutral400,
89 | },
90 | ] as StyleProp,
91 |
92 | bold: [
93 | {
94 | backgroundColor: colors.palette.bold500,
95 | tintColor: colors.palette.highlight500,
96 | borderColor: colors.palette.neutral700,
97 | borderWidth: 1,
98 | },
99 | ] as StyleProp,
100 | }
101 |
--------------------------------------------------------------------------------
/app/components/ButtonLink.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react"
2 | import { GestureResponderEvent, ViewStyle } from "react-native"
3 | import { AutoImage } from "./AutoImage"
4 | import { Button } from "./Button"
5 | import { spacing } from "../theme"
6 | import { openLinkInBrowser } from "../utils/openLinkInBrowser"
7 |
8 | type Props = {
9 | children: string
10 | style?: ViewStyle
11 | openLink: string | ((event: GestureResponderEvent) => void)
12 | preset?: "link" | "reversed" // "default" is not supported
13 | }
14 |
15 | export const ButtonLink: FC = ({ children, style, preset = "link", ...props }) => {
16 | const openLink = (e: GestureResponderEvent) => {
17 | if (typeof props.openLink === "function") {
18 | props.openLink(e)
19 | return
20 | }
21 | openLinkInBrowser(props.openLink)
22 | }
23 |
24 | return (
25 |
36 | )
37 | }
38 |
39 | const $arrow = {
40 | height: 24,
41 | width: 24,
42 | marginStart: spacing.extraSmall,
43 | }
44 |
--------------------------------------------------------------------------------
/app/components/ClosedBanner.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Text } from "./Text"
3 | import { View, type TextStyle, type ViewStyle } from "react-native"
4 | import { colors, spacing } from "../theme"
5 | import { translate } from "../i18n"
6 | import { openLinkInBrowser } from "../utils/openLinkInBrowser"
7 |
8 | export const ClosedBanner = () => {
9 | const chainReactWebsite = () =>
10 | openLinkInBrowser(`https://${translate("common.appClosedLinkText")}`)
11 |
12 | return (
13 |
14 |
15 | {translate("common.appClosedPart1")}
16 |
17 | {translate("common.appClosedPart2")}
18 |
19 |
20 | )
21 | }
22 |
23 | const $wrapper: ViewStyle = {
24 | backgroundColor: colors.errorBackground,
25 | padding: spacing.extraSmall,
26 | }
27 |
28 | const $text: TextStyle = {
29 | color: colors.error,
30 | textAlign: "center",
31 | }
32 |
33 | const $link: TextStyle = {
34 | color: colors.error,
35 | textDecorationColor: colors.error,
36 | textDecorationLine: "underline",
37 | }
38 |
--------------------------------------------------------------------------------
/app/components/CustomToast.tsx:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect } from "react"
2 | import { useSafeAreaInsets } from "react-native-safe-area-context"
3 | import { colors, spacing } from "../theme"
4 | import messaging from "@react-native-firebase/messaging"
5 | import Toast, { BaseToast, ToastConfig } from "react-native-toast-message"
6 | import { $baseSecondaryStyle, $baseStyle } from "./Text"
7 | import { ViewStyle, useWindowDimensions } from "react-native"
8 |
9 | // Setting up our custom Toast component
10 | export const CustomToast = () => {
11 | const insets = useSafeAreaInsets()
12 | const { width: screenWidth } = useWindowDimensions()
13 | const width = screenWidth - spacing.extraSmall * 2
14 |
15 | useLayoutEffect(() => {
16 | // handle a new push notification received while the app is in "foreground" state
17 | const unsubscribe = messaging().onMessage(async (remoteMessage) => {
18 | if (
19 | remoteMessage.notification &&
20 | (remoteMessage.notification.title || remoteMessage.notification.body)
21 | ) {
22 | Toast.show({
23 | text1: remoteMessage.notification.title,
24 | text2: remoteMessage.notification.body,
25 | })
26 | }
27 | })
28 | return unsubscribe
29 | })
30 |
31 | const toastConfig: ToastConfig = {
32 | success: (props) => (
33 |
40 | ),
41 | }
42 |
43 | return
44 | }
45 |
46 | const $toast: ViewStyle = {
47 | backgroundColor: colors.palette.neutral400,
48 | borderLeftWidth: 0,
49 | borderRadius: spacing.extraSmall,
50 | }
51 |
52 | const $toastContainer: ViewStyle = {
53 | paddingHorizontal: spacing.large,
54 | paddingVertical: spacing.medium,
55 | }
56 |
--------------------------------------------------------------------------------
/app/components/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React, { ComponentType } from "react"
2 | import {
3 | Image,
4 | ImageStyle,
5 | StyleProp,
6 | TouchableOpacity,
7 | TouchableOpacityProps,
8 | View,
9 | ViewStyle,
10 | } from "react-native"
11 |
12 | export type IconTypes = keyof typeof iconRegistry
13 |
14 | export interface IconProps extends TouchableOpacityProps {
15 | /**
16 | * The name of the icon
17 | */
18 | icon: IconTypes
19 |
20 | /**
21 | * An optional tint color for the icon
22 | */
23 | color?: string
24 |
25 | /**
26 | * An optional size for the icon. If not provided, the icon will be sized to the icon's resolution.
27 | */
28 | size?: number
29 |
30 | /**
31 | * Style overrides for the icon image
32 | */
33 | style?: StyleProp
34 |
35 | /**
36 | * Style overrides for the icon container
37 | */
38 | containerStyle?: StyleProp
39 |
40 | /**
41 | * An optional function to be called when the icon is pressed
42 | */
43 | onPress?: TouchableOpacityProps["onPress"]
44 | }
45 |
46 | /**
47 | * A component to render a registered icon.
48 | * It is wrapped in a if `onPress` is provided, otherwise a .
49 | *
50 | * - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Icon.md)
51 | */
52 | export function Icon(props: IconProps) {
53 | const {
54 | icon,
55 | color,
56 | size,
57 | style: $imageStyleOverride,
58 | containerStyle: $containerStyleOverride,
59 | ...WrapperProps
60 | } = props
61 |
62 | const isPressable = !!WrapperProps.onPress
63 | const Wrapper: ComponentType = (
64 | isPressable ? TouchableOpacity : View
65 | ) as ComponentType
66 |
67 | const $imageStyle: StyleProp = [
68 | $imageStyleBase,
69 | color ? { tintColor: color } : undefined,
70 | size ? { width: size, height: size } : undefined,
71 | $imageStyleOverride,
72 | ]
73 |
74 | return (
75 |
80 |
81 |
82 | )
83 | }
84 |
85 | export const iconRegistry = {
86 | arrow: require("../../assets/icons/arrows.png"),
87 | arrowDown: require("../../assets/icons/arrowDown.png"),
88 | arrowUp: require("../../assets/icons/arrowUp.png"),
89 | back: require("../../assets/icons/back.png"),
90 | caretLeft: require("../../assets/icons/caretLeft.png"),
91 | caretRight: require("../../assets/icons/caretRight.png"),
92 | chat: require("../../assets/icons/chat.png"),
93 | check: require("../../assets/icons/check.png"),
94 | explore: require("../../assets/icons/explore.png"),
95 | github: require("../../assets/icons/github.png"),
96 | info: require("../../assets/icons/info.png"),
97 | ladybug: require("../../assets/icons/ladybug.png"),
98 | link: require("../../assets/icons/link.png"),
99 | schedule: require("../../assets/icons/schedule.png"),
100 | twitter: require("../../assets/icons/twitter.png"),
101 | venue: require("../../assets/icons/venue.png"),
102 | youtube: require("../../assets/icons/youtube.png"),
103 | }
104 |
105 | const $imageStyleBase: ImageStyle = {
106 | resizeMode: "contain",
107 | }
108 |
--------------------------------------------------------------------------------
/app/components/MediaButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { colors } from "../theme"
3 | import { openLinkInBrowser } from "../utils/openLinkInBrowser"
4 | import { Icon } from "./Icon"
5 | import { FloatingButton, FloatingButtonProps } from "./Button"
6 |
7 | export interface MediaButtonProps extends FloatingButtonProps {
8 | /**
9 | * The url to open when the button is pressed.
10 | */
11 | talkURL: string
12 | /**
13 | * Whether the button should be visible or not when scrolling.
14 | */
15 | isScrolling: boolean
16 | }
17 |
18 | export function MediaButton(props: MediaButtonProps) {
19 | const { isScrolling, talkURL, ...rest } = props
20 |
21 | if (!talkURL) return null
22 |
23 | return (
24 | (
29 |
30 | )}
31 | TextProps={{ allowFontScaling: false }}
32 | onPress={() => openLinkInBrowser(talkURL)}
33 | {...rest}
34 | />
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/app/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Modal as RNModal, TextStyle, View, ViewStyle } from "react-native"
3 | import { TxKeyPath } from "../i18n"
4 | import { colors, spacing } from "../theme"
5 | import { Button, ButtonProps } from "./Button"
6 | import { Text } from "./Text"
7 |
8 | interface OnPressProps extends ButtonProps {
9 | cta: () => void
10 | label: TxKeyPath
11 | }
12 |
13 | interface ModalProps {
14 | title: TxKeyPath
15 | subtitle?: TxKeyPath
16 | confirmOnPress: OnPressProps
17 | cancelOnPress: OnPressProps
18 | isVisible?: boolean
19 | }
20 |
21 | export const Modal = ({
22 | title,
23 | subtitle,
24 | confirmOnPress,
25 | cancelOnPress,
26 | isVisible,
27 | }: ModalProps) => (
28 |
29 |
30 |
31 |
32 | {subtitle ? : null}
33 |
38 |
44 |
45 |
46 |
47 | )
48 |
49 | const $wrapper: ViewStyle = {
50 | flex: 1,
51 | alignItems: "center",
52 | justifyContent: "center",
53 | }
54 |
55 | const $card: ViewStyle = {
56 | backgroundColor: colors.palette.neutral100,
57 | borderRadius: spacing.medium,
58 | paddingHorizontal: spacing.large,
59 | paddingVertical: spacing.extraLarge,
60 | width: "80%",
61 | }
62 |
63 | const $textColor: TextStyle = {
64 | color: colors.palette.neutral800,
65 | }
66 |
67 | const $subtitle: TextStyle = {
68 | ...$textColor,
69 | marginTop: spacing.medium,
70 | }
71 |
72 | const $confirmButton: ViewStyle = {
73 | marginTop: spacing.large,
74 | }
75 |
76 | const $cancelButton: ViewStyle = {
77 | alignSelf: "center",
78 | marginTop: spacing.extraLarge,
79 | }
80 |
--------------------------------------------------------------------------------
/app/components/OTAUpdates.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 | import * as Updates from "expo-updates"
3 | import { reportCrash } from "../utils/crashReporting"
4 | import { useAppState } from "../hooks"
5 | import { Modal } from "./Modal"
6 |
7 | // Setting up our OTA Updates component
8 | export const OTAUpdates = () => {
9 | const [isModalVisible, setIsModalVisible] = useState(false)
10 | const [isUpdating, setIsUpdating] = useState(false)
11 |
12 | async function fetchAndRestartApp() {
13 | const fetchUpdate = await Updates.fetchUpdateAsync()
14 | if (fetchUpdate.isNew) {
15 | await Updates.reloadAsync()
16 | } else {
17 | setIsModalVisible(false)
18 | setIsUpdating(false)
19 | reportCrash("Fetch Update failed")
20 | }
21 | }
22 |
23 | async function onFetchUpdateAsync() {
24 | if (__DEV__ || process.env.NODE_ENV === "development") return
25 | try {
26 | const update = await Updates.checkForUpdateAsync()
27 | setIsModalVisible(update.isAvailable)
28 | } catch (error) {
29 | reportCrash(error)
30 | }
31 | }
32 |
33 | useAppState({
34 | match: /background/,
35 | nextAppState: "active",
36 | callback: onFetchUpdateAsync,
37 | })
38 |
39 | return (
40 | {
45 | setIsUpdating(true)
46 | await fetchAndRestartApp()
47 | },
48 | label: isUpdating ? "ota.confirmLabelUpdating" : "ota.confirmLabel",
49 | disabled: isUpdating,
50 | }}
51 | cancelOnPress={{
52 | cta: () => {
53 | setIsModalVisible(false)
54 | },
55 | label: "ota.cancelLabel",
56 | }}
57 | isVisible={isModalVisible}
58 | />
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/app/components/SafeAreaViewFixed.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react"
2 | import { StyleProp, View, ViewProps, ViewStyle } from "react-native"
3 | import { Edge, useSafeAreaInsets } from "react-native-safe-area-context"
4 |
5 | interface Props extends ViewProps {
6 | children: ReactNode
7 | style?: StyleProp
8 | edges?: Edge[]
9 | }
10 |
11 | /**
12 | * USE THIS - Alternative to the default [SafeAreaView](https://github.com/th3rdwave/react-native-safe-area-context#safeareaview)
13 | * from react-native-safe-area-context which currently has an issue that will cause a flicker / jump on first render on iOS / Android.
14 | *
15 | * [SafeAreaProvider](https://github.com/th3rdwave/react-native-safe-area-context#safeareaprovider) should still be higher in the tree.
16 | *
17 | * GitHub issues:
18 | * [219](https://github.com/th3rdwave/react-native-safe-area-context/issues/219),
19 | * [226](https://github.com/th3rdwave/react-native-safe-area-context/issues/226)
20 | */
21 | export default function SafeAreaViewFixed({ children, style, edges, ...rest }: Props) {
22 | const insets = useSafeAreaInsets()
23 | const defaultEdges = edges === undefined
24 | return (
25 |
37 | {children}
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/ScrollToButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useRef } from "react"
2 | import { ViewStyle, ViewToken, TextStyle, View } from "react-native"
3 | import { Button, ButtonProps } from "./Button"
4 | import { Icon } from "./Icon"
5 | import { colors, spacing } from "../theme"
6 | import Animated, {
7 | useAnimatedStyle,
8 | useDerivedValue,
9 | useSharedValue,
10 | withTiming,
11 | } from "react-native-reanimated"
12 | import { useFocusEffect } from "@react-navigation/native"
13 |
14 | export interface ScrollToButtonProps extends ButtonProps, ReturnType {
15 | navigateToCurrentEvent: () => void
16 | }
17 |
18 | const ARROW_SIZE = 24
19 |
20 | export function useScrollToEvent({
21 | lastEventIndex,
22 | scheduleIndex,
23 | }: {
24 | lastEventIndex: number
25 | scheduleIndex: number
26 | }) {
27 | const currentEventIndex = useSharedValue(-1)
28 | const currentlyViewingEvents = useSharedValue([])
29 | const currentlyViewingSchedule = useSharedValue(0)
30 | const isFocused = useSharedValue(false)
31 |
32 | useFocusEffect(
33 | useCallback(() => {
34 | isFocused.value = true
35 | return () => {
36 | isFocused.value = false
37 | }
38 | }, []),
39 | )
40 |
41 | const handleViewableEventIndexChanged = useRef(
42 | ({ viewableItems }: { viewableItems: ViewToken[] }) => {
43 | if (!isFocused.value) return
44 | currentlyViewingEvents.value = viewableItems
45 | .map((item) => item.index)
46 | .filter((i) => typeof i === "number") as Array
47 | },
48 | ).current
49 |
50 | const handleViewableScheduleIndexChanged = useRef(
51 | ({ viewableItems }: { viewableItems: ViewToken[] }) => {
52 | currentlyViewingSchedule.value = viewableItems[0].index ?? 0
53 | },
54 | ).current
55 |
56 | const scrollButtonOpacity = useDerivedValue(() => {
57 | const isScheduleVisible = scheduleIndex === currentlyViewingSchedule.value
58 | const isEventVisible = currentlyViewingEvents.value.includes(currentEventIndex.value)
59 | const isLastTwoEvents =
60 | [lastEventIndex, lastEventIndex - 1].includes(currentEventIndex.value) &&
61 | [lastEventIndex - 2].includes(currentlyViewingEvents.value[0])
62 | const isEventFirstVisible = currentlyViewingEvents.value[0] === currentEventIndex.value
63 | const hasCurrentEventIndex = currentEventIndex.value > -1
64 | const shouldShow = isLastTwoEvents ? !isEventVisible : !isEventFirstVisible
65 | return isScheduleVisible && hasCurrentEventIndex && shouldShow ? 1 : 0
66 | }, [lastEventIndex, scheduleIndex])
67 |
68 | const $scrollButtonStyle = useAnimatedStyle(() => ({
69 | opacity: scrollButtonOpacity.value,
70 | }))
71 |
72 | const $arrowStyle = useAnimatedStyle(
73 | () => ({
74 | transform: [
75 | {
76 | rotate: withTiming(
77 | currentlyViewingEvents.value[0] <= currentEventIndex.value ? "0deg" : "180deg",
78 | { duration: 0 },
79 | ),
80 | },
81 | ],
82 | }),
83 | [lastEventIndex],
84 | )
85 |
86 | return {
87 | $arrowStyle,
88 | currentEventIndex,
89 | currentlyViewingEvents,
90 | handleViewableEventIndexChanged,
91 | handleViewableScheduleIndexChanged,
92 | $scrollButtonStyle,
93 | }
94 | }
95 |
96 | export function ScrollToButton(props: ScrollToButtonProps) {
97 | const { $arrowStyle, $scrollButtonStyle, navigateToCurrentEvent, ...rest } = props
98 |
99 | return (
100 |
101 |
117 | )
118 | }
119 |
120 | const $scrollButtonContainer: ViewStyle = {
121 | position: "absolute",
122 | top: 0,
123 | alignSelf: "center",
124 | }
125 |
126 | const $scrollButton: ViewStyle = {
127 | borderColor: colors.palette.primary200,
128 | marginTop: spacing.extraSmall,
129 | paddingVertical: spacing.small,
130 | }
131 |
132 | const $scrollButtonText: TextStyle = {
133 | color: colors.palette.primary500,
134 | marginLeft: spacing.small + spacing.micro,
135 | }
136 |
137 | const $arrowContainer: ViewStyle = {
138 | height: ARROW_SIZE,
139 | width: ARROW_SIZE,
140 | }
141 |
142 | const $arrow: ViewStyle = {
143 | position: "absolute",
144 | top: 0,
145 | left: 0,
146 | }
147 |
--------------------------------------------------------------------------------
/app/components/SocialButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Pressable, PressableProps, StyleProp, ViewStyle } from "react-native"
3 | import { spacing } from "../theme"
4 | import { openLinkInBrowser } from "../utils/openLinkInBrowser"
5 | import { Icon, IconProps } from "./Icon"
6 |
7 | export interface SocialButtonProps extends PressableProps {
8 | /**
9 | * The icon to display.
10 | */
11 | icon: IconProps["icon"]
12 | /**
13 | * The size of the icon.
14 | */
15 | size?: number
16 | /**
17 | * Style overrides.
18 | */
19 | style?: StyleProp
20 | /**
21 | * The url to open when the button is pressed.
22 | */
23 | url?: string
24 | }
25 |
26 | export function SocialButton(props: SocialButtonProps) {
27 | const { size = 24, url, icon, style: $styleOverride, ...rest } = props
28 |
29 | if (!url) return null
30 |
31 | return (
32 | {
34 | openLinkInBrowser(url)
35 | }}
36 | style={[$socialButton, $styleOverride]}
37 | accessibilityRole="button"
38 | accessibilityLabel={icon}
39 | accessibilityHint={`Navigates to ${icon}`}
40 | {...rest}
41 | >
42 |
43 |
44 | )
45 | }
46 |
47 | interface SocialButtonsProps {
48 | socialButtons: Array
49 | }
50 |
51 | export const SocialButtons = ({ socialButtons }: SocialButtonsProps) => (
52 | <>
53 | {socialButtons.map((socialButtonProps, index) => (
54 | index && $socialButtons}
58 | />
59 | ))}
60 | >
61 | )
62 |
63 | const $socialButton: ViewStyle = {
64 | backgroundColor: "#1C2B3D",
65 | width: 42,
66 | height: 42,
67 | borderRadius: 21,
68 | alignItems: "center",
69 | justifyContent: "center",
70 | }
71 |
72 | const $socialButtons: ViewStyle = {
73 | marginEnd: spacing.medium,
74 | }
75 |
--------------------------------------------------------------------------------
/app/components/carousel/carousel.types.ts:
--------------------------------------------------------------------------------
1 | import { ImageSourcePropType, ImageStyle } from "react-native"
2 | import { ButtonProps } from "../Button"
3 | import { IconProps } from "../Icon"
4 |
5 | interface StaticCarouselProps {
6 | body: string
7 | button?: ButtonData & ButtonProps
8 | link?: { link: string; text: string }
9 | data: ImageSourcePropType[]
10 | meta?: string
11 | preset: "static"
12 | subtitle: string
13 | isBodySelectable?: boolean
14 | }
15 |
16 | export interface ButtonData {
17 | link: string
18 | text: string
19 | }
20 |
21 | export interface SocialButtonData {
22 | icon: IconProps["icon"]
23 | url?: string
24 | }
25 |
26 | export interface DynamicCarouselItem {
27 | body?: string
28 | image: ImageSourcePropType
29 | imageStyle?: ImageStyle
30 | label?: string
31 | leftButton?: ButtonData
32 | meta?: string
33 | rightButton?: ButtonData
34 | socialButtons?: SocialButtonData[]
35 | subtitle?: string
36 | }
37 |
38 | interface DynamicCarouselProps {
39 | preset: "dynamic"
40 | data: DynamicCarouselItem[]
41 | }
42 |
43 | export interface Spacer {
44 | spacer: number
45 | }
46 |
47 | export type CarouselProps =
48 | | (StaticCarouselProps | DynamicCarouselProps) & {
49 | openLink?: () => void
50 | carouselCardVariant?: "default" | "speaker"
51 | }
52 |
--------------------------------------------------------------------------------
/app/components/carousel/constants.ts:
--------------------------------------------------------------------------------
1 | import { Dimensions } from "react-native"
2 | import { layout, spacing } from "../../theme"
3 |
4 | // Note: This will not react to screen dimension changes post intial render
5 | const { width: screenWidth } = Dimensions.get("screen")
6 | const screenContentWidth = screenWidth - layout.horizontalGutter * 2
7 |
8 | // This sets the carousel card with to a percentage of the
9 | // content width so that the next card subtly shows next to it.
10 | export const CAROUSEL_CARD_WIDTH = Math.floor(screenContentWidth * 0.95)
11 | export const CAROUSEL_GAP = spacing.medium
12 | export const CAROUSEL_INTERVAL = CAROUSEL_CARD_WIDTH + CAROUSEL_GAP
13 | export const CAROUSEL_START_SPACER = layout.horizontalGutter
14 | // The ending spacer allows the carousel to scroll precisely to the
15 | // last card in the carousel based on the CAROUSEL_INTERVAL
16 | export const CAROUSEL_END_SPACER = screenContentWidth - CAROUSEL_INTERVAL + layout.horizontalGutter
17 |
--------------------------------------------------------------------------------
/app/components/carousel/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Carousel"
2 | export * from "./carousel.types"
3 | export * from "./CarouselCard"
4 | export * from "./constants"
5 |
--------------------------------------------------------------------------------
/app/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./AutoImage"
2 | export * from "./Avatar"
3 | export * from "./BoxShadow"
4 | export * from "./Button"
5 | export * from "./ButtonLink"
6 | export * from "./Card"
7 | export * from "./ClosedBanner"
8 | export * from "./carousel"
9 | export * from "./CustomToast"
10 | export * from "./Header"
11 | export * from "./Icon"
12 | export * from "./MediaButton"
13 | export * from "./Modal"
14 | export * from "./OTAUpdates"
15 | export * from "./Screen"
16 | export * from "./ScrollToButton"
17 | export * from "./SocialButton"
18 | export * from "./Text"
19 |
--------------------------------------------------------------------------------
/app/config.ts:
--------------------------------------------------------------------------------
1 | export interface ConfigBaseProps {
2 | persistNavigation: "always" | "dev" | "prod" | "never"
3 | catchErrors: "always" | "dev" | "prod" | "never"
4 | exitRoutes: string[]
5 | }
6 |
7 | export type PersistNavigationConfig = ConfigBaseProps["persistNavigation"]
8 |
9 | const BaseConfig: ConfigBaseProps = {
10 | // This feature is particularly useful in development mode, but
11 | // can be used in production as well if you prefer.
12 | persistNavigation: "always",
13 |
14 | /**
15 | * Only enable if we're catching errors in the right environment
16 | */
17 | catchErrors: "always",
18 |
19 | /**
20 | * This is a list of all the route names that will exit the app if the back button
21 | * is pressed while in that screen. Only affects Android.
22 | */
23 | exitRoutes: ["Welcome"],
24 | }
25 |
26 | export default BaseConfig
27 |
--------------------------------------------------------------------------------
/app/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useAppNavigation"
2 | export * from "./useAppState"
3 | export * from "./useCurrentDate"
4 | export * from "./useHeader"
5 | export * from "./useScrollHandlers"
6 |
--------------------------------------------------------------------------------
/app/hooks/useAppNavigation.ts:
--------------------------------------------------------------------------------
1 | import { ParamListBase, useNavigation } from "@react-navigation/native"
2 | import { NativeStackNavigationProp } from "@react-navigation/native-stack"
3 | import { AppStackParamList } from "../navigators"
4 |
5 | export function useAppNavigation() {
6 | const navigation = useNavigation>()
7 |
8 | return navigation
9 | }
10 |
--------------------------------------------------------------------------------
/app/hooks/useAppState.ts:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { AppState, AppStateStatus } from "react-native"
3 |
4 | type UseAppStateProps = {
5 | match: RegExp
6 | nextAppState: AppStateStatus
7 | callback: () => void
8 | }
9 |
10 | export function useAppState({ match, nextAppState, callback }: UseAppStateProps) {
11 | const appState = React.useRef(AppState.currentState)
12 | const [_, setAppStateVisible] = React.useState(appState.current)
13 |
14 | React.useEffect(() => {
15 | // First time check (opening App from a killed state)
16 | if (appState.current === nextAppState) {
17 | callback()
18 | }
19 |
20 | // Set up event listener
21 | const subscription = AppState.addEventListener("change", _handleAppStateChange)
22 | return () => {
23 | subscription.remove()
24 | }
25 | }, [])
26 |
27 | const _handleAppStateChange = (newAppState: AppStateStatus) => {
28 | // If the state we're coming from matches and
29 | // the next state is the desired one, fire callback
30 | if (appState.current.match(match) && newAppState === nextAppState) {
31 | callback()
32 | }
33 |
34 | appState.current = newAppState
35 | setAppStateVisible(appState.current)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/hooks/useCurrentDate.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { loadString } from "../utils/storage"
3 |
4 | const isValidDateString = (date: string) => {
5 | const d = new Date(date)
6 | return d instanceof Date && !isNaN(d.getTime())
7 | }
8 |
9 | /**
10 | * Check async storage the `currentDate` key on mount for a valid date value.
11 | * If it's not there, or it's not a valid date, then we'll set it to the current date.
12 | */
13 | export function useCurrentDate() {
14 | const [currentDate, setCurrentDate] = useState(new Date())
15 |
16 | useEffect(() => {
17 | async function checkCurrentDate() {
18 | const date = await loadString("currentDate")
19 | if (date && isValidDateString(date)) {
20 | setCurrentDate(new Date(date))
21 | }
22 | }
23 | checkCurrentDate()
24 | }, [])
25 |
26 | return currentDate
27 | }
28 |
--------------------------------------------------------------------------------
/app/hooks/useHeader.tsx:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect } from "react"
2 | import { useNavigation } from "@react-navigation/native"
3 | import { Header, HeaderProps } from "../components/Header"
4 |
5 | /**
6 | * A hook to set the header for a screen.
7 | */
8 | export function useHeader(headerProps: HeaderProps, deps: any[] = []) {
9 | const navigation = useNavigation()
10 |
11 | useLayoutEffect(() => {
12 | navigation.setOptions({
13 | headerShown: true,
14 | header: () => ,
15 | })
16 | }, deps)
17 | }
18 |
--------------------------------------------------------------------------------
/app/hooks/useScrollHandlers.ts:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated"
3 |
4 | enum Scroll {
5 | onMomentumScrollBegin = "onMomentumScrollBegin",
6 | onMomentumScrollEnd = "onMomentumScrollEnd",
7 | onScrollBeginDrag = "onScrollBeginDrag",
8 | onScrollEndDrag = "onScrollEndDrag",
9 | }
10 |
11 | export const useFloatingActionEvents = () => {
12 | const [scrollState, setScrollState] = React.useState(null)
13 | const [isScrolling, setIsScrolling] = React.useState(false)
14 |
15 | const scrollHandlers = {
16 | [Scroll.onMomentumScrollBegin]: () => setScrollState(Scroll.onMomentumScrollBegin),
17 | [Scroll.onMomentumScrollEnd]: () => setScrollState(Scroll.onMomentumScrollEnd),
18 | [Scroll.onScrollBeginDrag]: () => setScrollState(Scroll.onScrollBeginDrag),
19 | [Scroll.onScrollEndDrag]: () => setScrollState(Scroll.onScrollEndDrag),
20 | }
21 |
22 | React.useEffect(() => {
23 | let timeout!: ReturnType
24 |
25 | if (scrollState === Scroll.onScrollBeginDrag) {
26 | setIsScrolling(true)
27 | }
28 |
29 | if (scrollState === Scroll.onMomentumScrollBegin) {
30 | clearTimeout(timeout)
31 | }
32 |
33 | if (scrollState === Scroll.onMomentumScrollEnd || scrollState === Scroll.onScrollEndDrag) {
34 | timeout = setTimeout(() => {
35 | setIsScrolling(false)
36 | }, 500)
37 | }
38 |
39 | return () => clearTimeout(timeout)
40 | }, [scrollState])
41 |
42 | return {
43 | isScrolling,
44 | scrollHandlers,
45 | }
46 | }
47 |
48 | export const useScrollY = () => {
49 | const scrollY = useSharedValue(0)
50 | const scrollHandler = useAnimatedScrollHandler({
51 | onScroll: (event) => {
52 | scrollY.value = event.contentOffset.y
53 | },
54 | })
55 | return { scrollY, scrollHandler }
56 | }
57 |
--------------------------------------------------------------------------------
/app/i18n/i18n.ts:
--------------------------------------------------------------------------------
1 | import * as Localization from "expo-localization"
2 | import i18n from "i18n-js"
3 | import { I18nManager } from "react-native"
4 |
5 | // if English isn't your default language, move Translations to the appropriate language file.
6 | import en, { Translations } from "./en"
7 |
8 | i18n.fallbacks = true
9 | /**
10 | * we need always include "*-US" for some valid language codes because when you change the system language,
11 | * the language code is the suffixed with "-US". i.e. if a device is set to English ("en"),
12 | * if you change to another language and then return to English language code is now "en-US".
13 | */
14 | i18n.translations = { en, "en-US": en }
15 |
16 | i18n.locale = Localization.locale
17 |
18 | // handle RTL languages
19 | export const isRTL = Localization.isRTL
20 | I18nManager.allowRTL(isRTL)
21 | I18nManager.forceRTL(isRTL)
22 |
23 | /**
24 | * Builds up valid keypaths for translations.
25 | */
26 | export type TxKeyPath = RecursiveKeyOf
27 |
28 | // via: https://stackoverflow.com/a/65333050
29 | type RecursiveKeyOf = {
30 | [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue
31 | }[keyof TObj & (string | number)]
32 |
33 | type RecursiveKeyOfInner = {
34 | [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<
35 | TObj[TKey],
36 | `['${TKey}']` | `.${TKey}`
37 | >
38 | }[keyof TObj & (string | number)]
39 |
40 | type RecursiveKeyOfHandleValue = TValue extends any[]
41 | ? Text
42 | : TValue extends object
43 | ? Text | `${Text}${RecursiveKeyOfInner}`
44 | : Text
45 |
--------------------------------------------------------------------------------
/app/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import "./i18n"
2 | export * from "./i18n"
3 | export * from "./translate"
4 |
--------------------------------------------------------------------------------
/app/i18n/translate.ts:
--------------------------------------------------------------------------------
1 | import i18n from "i18n-js"
2 | import { TxKeyPath } from "./i18n"
3 |
4 | /**
5 | * Translates text.
6 | *
7 | * @param key The i18n key.
8 | * @param options The i18n options.
9 | * @returns The translated text.
10 | *
11 | * @example
12 | * Translations:
13 | *
14 | * ```en.ts
15 | * {
16 | * "hello": "Hello, {{name}}!"
17 | * }
18 | * ```
19 | *
20 | * Usage:
21 | * ```ts
22 | * import { translate } from "i18n-js"
23 | *
24 | * translate("common.ok", { name: "world" })
25 | * // => "Hello world!"
26 | * ```
27 | */
28 | export function translate(key: TxKeyPath, options?: i18n.TranslateOptions) {
29 | return i18n.t(key, options)
30 | }
31 |
--------------------------------------------------------------------------------
/app/navigators/AppNavigator.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The app navigator (formerly "AppNavigator" and "MainNavigator") is used for the primary
3 | * navigation flows of your app.
4 | * Usually this will contain an auth flow (registration, login, forgot password)
5 | * and a "main" flow which the user will use once logged in, but in this case we
6 | * just have a single flow.
7 | */
8 | import {
9 | DarkTheme,
10 | DefaultTheme,
11 | NavigationContainer,
12 | NavigatorScreenParams,
13 | } from "@react-navigation/native"
14 | import { createNativeStackNavigator } from "@react-navigation/native-stack"
15 | import { StackScreenProps } from "@react-navigation/stack"
16 | import React from "react"
17 | import { useColorScheme } from "react-native"
18 | import Config from "../config"
19 | import { DebugScreen, TalkDetailsScreen, WelcomeScreen, WorkshopDetailsScreen } from "../screens"
20 | import { BreakDetailsScreen } from "../screens/TalkDetailsScreen/BreakDetailsScreen"
21 | import { colors } from "../theme"
22 | import { navigationRef, useBackButtonHandler } from "./navigationUtilities"
23 | import { TabNavigator, TabParamList } from "./TabNavigator"
24 |
25 | /**
26 | * This type allows TypeScript to know what routes are defined in this navigator
27 | * as well as what properties (if any) they might take when navigating to them.
28 | *
29 | * If no params are allowed, pass through `undefined`. Generally speaking, we
30 | * recommend using your MobX-State-Tree store(s) to keep application state
31 | * rather than passing state through navigation params.
32 | *
33 | * For more information, see this documentation:
34 | * https://reactnavigation.org/docs/params/
35 | * https://reactnavigation.org/docs/typescript#type-checking-the-navigator
36 | * https://reactnavigation.org/docs/typescript/#organizing-types
37 | */
38 | export type AppStackParamList = {
39 | Debug: undefined
40 | Tabs: NavigatorScreenParams
41 | TalkDetails: { scheduleId: string }
42 | Welcome: undefined
43 | WorkshopDetails: { scheduleId: string }
44 | BreakDetails: { scheduleId: string }
45 | }
46 |
47 | /**
48 | * This is a list of all the route names that will exit the app if the back button
49 | * is pressed while in that screen. Only affects Android.
50 | */
51 | const exitRoutes = Config.exitRoutes
52 |
53 | export type AppStackScreenProps = StackScreenProps<
54 | AppStackParamList,
55 | T
56 | >
57 |
58 | // Documentation: https://reactnavigation.org/docs/stack-navigator/
59 | const Stack = createNativeStackNavigator()
60 |
61 | const AppStack = () => {
62 | return (
63 |
67 |
68 |
69 |
74 |
75 |
80 |
85 |
86 | )
87 | }
88 |
89 | interface NavigationProps extends Partial> {}
90 |
91 | export const AppNavigator = (props: NavigationProps) => {
92 | const colorScheme = useColorScheme()
93 |
94 | useBackButtonHandler((routeName) => exitRoutes.includes(routeName))
95 |
96 | const navTheme = React.useMemo(() => {
97 | const theme = colorScheme === "dark" ? DarkTheme : DefaultTheme
98 | return {
99 | ...theme,
100 | colors: {
101 | ...theme.colors,
102 | background: colors.background,
103 | },
104 | }
105 | }, [colorScheme])
106 |
107 | return (
108 |
109 |
110 |
111 | )
112 | }
113 |
--------------------------------------------------------------------------------
/app/navigators/BackButton.tsx:
--------------------------------------------------------------------------------
1 | // A component which implements a back button for the navigation bar
2 | //
3 | import React from "react"
4 | import { HeaderAction } from "../components"
5 | import { useAppNavigation } from "../hooks"
6 |
7 | export function BackButton() {
8 | const navigation = useAppNavigation()
9 | return
10 | }
11 |
--------------------------------------------------------------------------------
/app/navigators/InfoStackNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { createNativeStackNavigator } from "@react-navigation/native-stack"
3 | import { InfoScreen } from "../screens"
4 | import { CodeOfConductScreen } from "../screens/InfoScreen/CodeOfConductScreen"
5 | import { ContactUsScreen } from "../screens/InfoScreen/ContactUsScreen"
6 | import { OurSponsorsScreen } from "../screens/InfoScreen/OurSponsorsScreen"
7 | import { CreditsScreen } from "../screens/InfoScreen/CreditsScreen"
8 | import { StackScreenProps } from "@react-navigation/stack"
9 |
10 | export type InfoStackParamList = {
11 | Info: undefined
12 | CodeOfConduct: undefined
13 | ContactUs: undefined
14 | OurSponsors: undefined
15 | Credits: undefined
16 | }
17 |
18 | const Stack = createNativeStackNavigator()
19 |
20 | export type InfoStackScreenProps = StackScreenProps<
21 | InfoStackParamList,
22 | T
23 | >
24 |
25 | export const InfoStackNavigator = () => {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/app/navigators/TabNavigator.tsx:
--------------------------------------------------------------------------------
1 | import { BottomTabScreenProps, createBottomTabNavigator } from "@react-navigation/bottom-tabs"
2 | import { CompositeScreenProps } from "@react-navigation/native"
3 | import React, { ComponentType, FC } from "react"
4 | import { TextStyle, ViewStyle } from "react-native"
5 | import { useSafeAreaInsets } from "react-native-safe-area-context"
6 | import { Icon, IconTypes } from "../components"
7 | import { translate, TxKeyPath } from "../i18n"
8 | import { ExploreScreen, ScheduleScreen, VenuesScreen, ChatScreen } from "../screens"
9 | import { colors, layout, spacing, typography } from "../theme"
10 | import { AppStackParamList, AppStackScreenProps } from "./AppNavigator"
11 | import { InfoStackNavigator } from "./InfoStackNavigator"
12 |
13 | export type TabParamList = {
14 | Schedule: undefined
15 | Venues: undefined
16 | Explore: undefined
17 | InfoStack: undefined
18 | Chat: undefined
19 | }
20 |
21 | /**
22 | * Helper for automatically generating navigation prop types for each route.
23 | *
24 | * More info: https://reactnavigation.org/docs/typescript/#organizing-types
25 | */
26 | export type TabScreenProps = CompositeScreenProps<
27 | BottomTabScreenProps,
28 | AppStackScreenProps
29 | >
30 |
31 | const Tab = createBottomTabNavigator()
32 |
33 | type ScreenComponentType =
34 | | FC>
35 | | FC>
36 | | FC>
37 | | FC>
38 | | ComponentType
39 |
40 | interface Screen {
41 | name: keyof TabParamList
42 | component: ScreenComponentType
43 | txLabel: TxKeyPath
44 | icon: IconTypes
45 | }
46 |
47 | const screens: Screen[] = [
48 | {
49 | name: "Schedule",
50 | component: ScheduleScreen,
51 | txLabel: "tabNavigator.scheduleTab",
52 | icon: "schedule",
53 | },
54 | {
55 | name: "Venues",
56 | component: VenuesScreen,
57 | txLabel: "tabNavigator.venuesTab",
58 | icon: "venue",
59 | },
60 | {
61 | name: "Explore",
62 | component: ExploreScreen,
63 | txLabel: "tabNavigator.exploreTab",
64 | icon: "explore",
65 | },
66 | {
67 | name: "InfoStack",
68 | component: InfoStackNavigator,
69 | txLabel: "tabNavigator.infoTab",
70 | icon: "info",
71 | },
72 | {
73 | name: "Chat",
74 | component: ChatScreen,
75 | txLabel: "tabNavigator.chatTab",
76 | icon: "chat",
77 | },
78 | ]
79 |
80 | export function TabNavigator() {
81 | const { bottom } = useSafeAreaInsets()
82 |
83 | return (
84 |
96 | {screens.map((screen) => (
97 | (
104 |
105 | ),
106 | }}
107 | />
108 | ))}
109 |
110 | )
111 | }
112 |
113 | const $tabBar: ViewStyle = {
114 | backgroundColor: colors.background,
115 | borderTopColor: colors.transparent,
116 | }
117 |
118 | const $tabBarItem: ViewStyle = {
119 | paddingTop: spacing.medium,
120 | }
121 |
122 | const $tabBarLabel: TextStyle = {
123 | fontSize: 12,
124 | fontFamily: typography.primary.medium,
125 | lineHeight: 16,
126 | flex: 1,
127 | }
128 |
--------------------------------------------------------------------------------
/app/navigators/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./AppNavigator"
2 | export * from "./navigationUtilities"
3 | // export other navigators from here
4 |
--------------------------------------------------------------------------------
/app/navigators/navigationUtilities.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from "react"
2 | import { BackHandler, Platform } from "react-native"
3 | import {
4 | PartialState,
5 | NavigationState,
6 | NavigationAction,
7 | createNavigationContainerRef,
8 | } from "@react-navigation/native"
9 | import Config, { type PersistNavigationConfig } from "../config"
10 | import { useIsMounted } from "../utils/isMounted"
11 |
12 | /* eslint-disable */
13 | export const RootNavigation = {
14 | navigate(_name: string, _params?: any) {},
15 | goBack() {},
16 | resetRoot(_state?: PartialState | NavigationState) {},
17 | getRootState(): NavigationState {
18 | return {} as any
19 | },
20 | dispatch(_action: NavigationAction) {},
21 | }
22 | /* eslint-enable */
23 |
24 | export const navigationRef = createNavigationContainerRef()
25 |
26 | /**
27 | * Gets the current screen from any navigation state.
28 | */
29 | export function getActiveRouteName(state: NavigationState | PartialState): string {
30 | const route = state.routes[state.index ?? 0]
31 |
32 | // Found the active route -- return the name
33 | if (!route.state) return route.name
34 |
35 | // Recursive call to deal with nested routers
36 | return getActiveRouteName(route.state)
37 | }
38 |
39 | /**
40 | * Hook that handles Android back button presses and forwards those on to
41 | * the navigation or allows exiting the app.
42 | */
43 | export function useBackButtonHandler(canExit: (routeName: string) => boolean) {
44 | // ignore if iOS ... no back button!
45 | if (Platform.OS === "ios") return
46 |
47 | // The reason we're using a ref here is because we need to be able
48 | // to update the canExit function without re-setting up all the listeners
49 | const canExitRef = useRef(canExit)
50 |
51 | useEffect(() => {
52 | canExitRef.current = canExit
53 | }, [canExit])
54 |
55 | useEffect(() => {
56 | // We'll fire this when the back button is pressed on Android.
57 | const onBackPress = () => {
58 | if (!navigationRef.isReady()) {
59 | return false
60 | }
61 |
62 | // grab the current route
63 | const routeName = getActiveRouteName(navigationRef.getRootState())
64 |
65 | // are we allowed to exit?
66 | if (canExitRef.current(routeName)) {
67 | // exit and let the system know we've handled the event
68 | BackHandler.exitApp()
69 | return true
70 | }
71 |
72 | // we can't exit, so let's turn this into a back action
73 | if (navigationRef.canGoBack()) {
74 | navigationRef.goBack()
75 | return true
76 | }
77 |
78 | return false
79 | }
80 |
81 | // Subscribe when we come to life
82 | BackHandler.addEventListener("hardwareBackPress", onBackPress)
83 |
84 | // Unsubscribe when we're done
85 | return () => BackHandler.removeEventListener("hardwareBackPress", onBackPress)
86 | }, [])
87 | }
88 |
89 | /**
90 | * This helper function will determine whether we should enable navigation persistence
91 | * based on a config setting and the __DEV__ environment (dev or prod).
92 | */
93 | function navigationRestoredDefaultState(persistNavigation: PersistNavigationConfig) {
94 | if (persistNavigation === "always") return false
95 | if (persistNavigation === "dev" && __DEV__) return false
96 | if (persistNavigation === "prod" && !__DEV__) return false
97 |
98 | // all other cases, disable restoration by returning true
99 | return true
100 | }
101 |
102 | /**
103 | * Custom hook for persisting navigation state.
104 | */
105 | export function useNavigationPersistence(storage: any, persistenceKey: string) {
106 | const [initialNavigationState, setInitialNavigationState] = useState()
107 | const isMounted = useIsMounted()
108 |
109 | const initNavState = navigationRestoredDefaultState(Config.persistNavigation)
110 | const [isRestored, setIsRestored] = useState(initNavState)
111 |
112 | const routeNameRef = useRef()
113 |
114 | const onNavigationStateChange = (state: NavigationState | undefined): void => {
115 | const previousRouteName = routeNameRef.current
116 | if (state) {
117 | const currentRouteName = getActiveRouteName(state)
118 |
119 | if (previousRouteName !== currentRouteName) {
120 | // track screens.
121 | if (__DEV__ || process.env.NODE_ENV === "development") {
122 | console.tron.log(currentRouteName)
123 | }
124 |
125 | // Save the current route name for later comparision
126 | routeNameRef.current = currentRouteName
127 |
128 | // Persist state to storage
129 | storage.save(persistenceKey, state)
130 | }
131 | }
132 | }
133 |
134 | const restoreState = async () => {
135 | try {
136 | const state = await storage.load(persistenceKey)
137 | if (state) setInitialNavigationState(state)
138 | } finally {
139 | if (isMounted()) setIsRestored(true)
140 | }
141 | }
142 |
143 | useEffect(() => {
144 | if (!isRestored) restoreState()
145 | }, [isRestored])
146 |
147 | return { onNavigationStateChange, restoreState, isRestored, initialNavigationState }
148 | }
149 |
150 | /**
151 | * use this to navigate without the navigation
152 | * prop. If you have access to the navigation prop, do not use this.
153 | * More info: https://reactnavigation.org/docs/navigating-without-navigation-prop/
154 | */
155 | export function navigate(name: any, params?: any) {
156 | if (navigationRef.isReady()) {
157 | navigationRef.navigate(name as never, params as never)
158 | }
159 | }
160 |
161 | export function goBack() {
162 | if (navigationRef.isReady() && navigationRef.canGoBack()) {
163 | navigationRef.goBack()
164 | }
165 | }
166 |
167 | export function resetRoot(params = { index: 0, routes: [] }) {
168 | if (navigationRef.isReady()) {
169 | navigationRef.resetRoot(params)
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/app/screens/ChatScreen/ChatScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react"
2 | import { useHeader } from "../../hooks"
3 | import { TabScreenProps } from "../../navigators/TabNavigator"
4 | import { translate } from "../../i18n"
5 | import { GiftedChat, IMessage } from "react-native-gifted-chat"
6 | import AsyncStorage from "@react-native-async-storage/async-storage"
7 | import { aiPrompt } from "./ai"
8 | import { View, ViewStyle } from "react-native"
9 |
10 | const NUMBER_OF_MESSAGES_TO_SEND = 8
11 | const chatbotAvatarURL =
12 | "https://pbs.twimg.com/profile_images/1638277840352456704/g7sdYc76_400x400.jpg"
13 | const chatbotName = "Chain React Bot"
14 |
15 | const INITIAL_MESSAGES: IMessage[] = [
16 | {
17 | _id: 1,
18 | text: `Hello there! I'm ${chatbotName}. How can I help you with questions about the Chain React conference and the surrounding Portland area? Please note I'm an experimental feature and if something goes wrong, just let an Infinite Red team member know!`,
19 | createdAt: new Date(),
20 | user: {
21 | _id: 2,
22 | name: chatbotName,
23 | avatar: chatbotAvatarURL,
24 | },
25 | },
26 | ]
27 |
28 | export const ChatScreen: React.FunctionComponent> = () => {
29 | useHeader({
30 | title: translate("chatScreen.title"),
31 | rightTx: "chatScreen.reset",
32 | onRightPress: () => {
33 | AsyncStorage.removeItem("chat/messages")
34 | setMessages(INITIAL_MESSAGES)
35 | },
36 | })
37 |
38 | // generate a user-specific uuid if it isn't already in AsyncStorage
39 | const [uuid, setUuid] = useState(null)
40 |
41 | // show a "typing" indicator when Claude is responding -- initially true
42 | const [typingIndicator, setTypingIndicator] = useState(true)
43 |
44 | // also grab messages in AsyncStorage
45 | const [messages, setMessages] = useState([])
46 |
47 | useEffect(() => {
48 | const getUuid = async () => {
49 | const uuid = await AsyncStorage.getItem("chat/uuid")
50 | if (uuid) {
51 | setUuid(uuid)
52 | } else {
53 | // generate a random string
54 | const uuid =
55 | Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
56 | await AsyncStorage.setItem("chat/uuid", uuid)
57 | setUuid(uuid)
58 | }
59 | }
60 | getUuid()
61 |
62 | const getMessages = async () => {
63 | const messages = await AsyncStorage.getItem("chat/messages")
64 | if (messages) {
65 | setMessages(JSON.parse(messages))
66 | } else {
67 | setMessages(INITIAL_MESSAGES)
68 | }
69 |
70 | setTypingIndicator(false)
71 | }
72 | getMessages()
73 | }, [])
74 |
75 | async function saveMessages(messagesCallback: (old: IMessage[]) => IMessage[]) {
76 | let newMessages: IMessage[] = []
77 | setMessages((old) => {
78 | newMessages = messagesCallback(old)
79 | return newMessages
80 | })
81 |
82 | // persist to AsyncStorage
83 | AsyncStorage.setItem("chat/messages", JSON.stringify(newMessages))
84 | }
85 |
86 | return (
87 | <>
88 | {uuid && (
89 |
90 | {
94 | const appendedMessages = [
95 | ...newMessages.map((message) => ({ ...message, _id: Date.now() })),
96 | ...messages,
97 | ]
98 | saveMessages(() => appendedMessages)
99 | setTypingIndicator(true)
100 |
101 | // ask Claude for AI response
102 | // first, build the prompt using the last 15 messages
103 | const prompt = appendedMessages
104 | .slice(0, NUMBER_OF_MESSAGES_TO_SEND)
105 | .reverse()
106 | .map(
107 | (message) =>
108 | (message.user._id === uuid ? "Human: " : "Assistant: ") + message.text,
109 | )
110 | .join("\n\n")
111 |
112 | // turn on the GiftedChat "typing" indicator
113 | // then, ask Claude for a response
114 | const response = await aiPrompt({ prompt, userId: uuid })
115 | const claudeMessage = {
116 | _id: Date.now(),
117 | text: response.completion.trim(),
118 | createdAt: new Date(),
119 | user: {
120 | _id: 2,
121 | name: chatbotName,
122 | avatar: chatbotAvatarURL,
123 | },
124 | }
125 | setTypingIndicator(false)
126 | saveMessages(() => [claudeMessage, ...appendedMessages])
127 | }}
128 | user={{ _id: uuid }}
129 | textInputProps={{ testID: "aiChatInput" }}
130 | listViewProps={{ keyboardDismissMode: "on-drag" }}
131 | />
132 |
133 | )}
134 | >
135 | )
136 | }
137 |
138 | const $root: ViewStyle = {
139 | flex: 1,
140 | }
141 |
--------------------------------------------------------------------------------
/app/screens/ChatScreen/ai.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 | import { reportCrash } from "../../utils/crashReporting"
3 |
4 | export async function aiPrompt({ prompt, userId }: { prompt: string; userId: string }) {
5 | try {
6 | const response = await axios.post<{ completion: string }>(
7 | // "http://localhost:3000/api/claude",
8 | "https://chain-react-ai-chat.vercel.app/api/claude",
9 | {
10 | prompt,
11 | userId,
12 | },
13 | {
14 | headers: {
15 | "content-type": "application/json",
16 | },
17 | },
18 | )
19 |
20 | return response.data
21 | } catch (error) {
22 | reportCrash(error)
23 | return { completion: "Hm, I seem to be having some trouble right now." }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/screens/DebugScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react"
2 | import { TextStyle, View, ViewStyle } from "react-native"
3 | import { StackScreenProps } from "@react-navigation/stack"
4 | import { AppStackParamList } from "../navigators/AppNavigator"
5 | import { resetRoot } from "../navigators/navigationUtilities"
6 | import { Button, Screen, Text } from "../components"
7 |
8 | import messaging from "@react-native-firebase/messaging"
9 | import { spacing } from "../theme"
10 | import { useAppNavigation, useHeader } from "../hooks"
11 | import { translate } from "../i18n"
12 | import { clear } from "../utils/storage"
13 | import { useQueryClient } from "@tanstack/react-query"
14 | import { BackButton } from "../navigators/BackButton"
15 | import * as Updates from "expo-updates"
16 | import * as Application from "expo-application"
17 |
18 | export const DebugScreen: FC> = () => {
19 | const navigation = useAppNavigation()
20 | useHeader({
21 | LeftActionComponent: ,
22 | title: translate("debugScreen.title"),
23 | })
24 | const [fcmToken, setFcmToken] = React.useState(undefined)
25 | const queryClient = useQueryClient()
26 |
27 | const clearState = () => {
28 | // Clear react-query state
29 | queryClient.resetQueries()
30 |
31 | // Clear async storage
32 | clear()
33 |
34 | // Clear navigation state
35 | resetRoot()
36 | navigation.navigate("Welcome")
37 | }
38 |
39 | React.useEffect(() => {
40 | const getToken = async () => {
41 | const token = await messaging().getToken()
42 | setFcmToken(token)
43 | }
44 | getToken()
45 | }, [])
46 |
47 | return (
48 |
54 |
55 |
56 |
57 |
63 |
64 |
65 | {(Updates.channel ?? "").length > 0 ? (
66 |
67 | ) : null}
68 | {Updates.updateId ? (
69 |
70 | ) : null}
71 |
76 |
77 |
78 | )
79 | }
80 |
81 | const $root: ViewStyle = {
82 | flex: 1,
83 | paddingHorizontal: spacing.large,
84 | }
85 |
86 | const $rootContainer: ViewStyle = {
87 | flex: 1,
88 | justifyContent: "space-between",
89 | }
90 |
91 | const $resetStateButtonShadow: ViewStyle = {
92 | marginTop: spacing.large,
93 | }
94 |
95 | const $subtitle: TextStyle = {
96 | marginBottom: spacing.medium,
97 | }
98 |
99 | const $footer: ViewStyle = {
100 | marginBottom: spacing.medium,
101 | }
102 |
103 | const $spec: TextStyle = {
104 | marginTop: spacing.extraSmall,
105 | }
106 |
--------------------------------------------------------------------------------
/app/screens/ErrorScreen/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, ErrorInfo, ReactNode } from "react"
2 | import { reportCrash } from "../../utils/crashReporting"
3 | import { ErrorDetails, ErrorDetailsProps } from "./ErrorDetails"
4 |
5 | interface Props {
6 | children: ReactNode
7 | catchErrors: "always" | "dev" | "prod" | "never"
8 | onReset(): void
9 | }
10 |
11 | interface State {
12 | error: Error | null
13 | errorInfo: ErrorInfo | null
14 | }
15 |
16 | /**
17 | * This component handles whenever the user encounters a JS error in the
18 | * app. It follows the "error boundary" pattern in React. We're using a
19 | * class component because according to the documentation, only class
20 | * components can be error boundaries.
21 | *
22 | * - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Error-Boundary.md)
23 | * - [React Error Boundaries](https://reactjs.org/docs/error-boundaries.html)
24 | */
25 | export class ErrorBoundary extends Component {
26 | state: Omit = { error: null, errorInfo: null }
27 |
28 | // If an error in a child is encountered, this will run
29 | componentDidCatch(error: Error, errorInfo: ErrorInfo) {
30 | // Catch errors in any components below and re-render with error message
31 | this.setState({
32 | error,
33 | errorInfo,
34 | })
35 |
36 | // You can also log error messages to an error reporting service here
37 | // This is a great place to put BugSnag, Sentry, crashlytics, etc:
38 | reportCrash(error)
39 | }
40 |
41 | // Reset the error back to null
42 | resetError = () => {
43 | this.props.onReset()
44 | this.setState({ error: null, errorInfo: null })
45 | }
46 |
47 | // To avoid unnecessary re-renders
48 | shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean {
49 | return nextState.error !== nextProps.error
50 | }
51 |
52 | // Only enable if we're catching errors in the right environment
53 | isEnabled(): boolean {
54 | return (
55 | this.props.catchErrors === "always" ||
56 | (this.props.catchErrors === "dev" && __DEV__) ||
57 | (this.props.catchErrors === "prod" && !__DEV__)
58 | )
59 | }
60 |
61 | // Render an error UI if there's an error; otherwise, render children
62 | render() {
63 | return this.isEnabled() && this.state.error ? (
64 |
69 | ) : (
70 | this.props.children
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/screens/ErrorScreen/ErrorDetails.tsx:
--------------------------------------------------------------------------------
1 | import React, { ErrorInfo } from "react"
2 | import { ScrollView, TextStyle, View, ViewStyle } from "react-native"
3 | import { Button, Icon, Screen, Text } from "../../components"
4 | import { colors, spacing } from "../../theme"
5 | import { openLinkInBrowser } from "../../utils/openLinkInBrowser"
6 |
7 | export interface ErrorDetailsProps {
8 | error: Error | null
9 | errorInfo: ErrorInfo | null
10 | onReset(): void
11 | }
12 |
13 | export function ErrorDetails(props: ErrorDetailsProps) {
14 | const { error, errorInfo } = props
15 | const errorTitle = `${error}`.trim()
16 | // Issue body that is the first 10 lines of the error stack
17 | const issueBodyStacktrace = errorInfo?.componentStack.split("\n").slice(0, 10).join("\n")
18 | const githubURL = encodeURI(
19 | `https://github.com/infinitered/ChainReactApp2023/issues/new?title=(CRASH) ${errorTitle}&body=What were you doing when the app crashed?\n\n\nTruncated Stacktrace:\n\`\`\`${issueBodyStacktrace}\`\`\``,
20 | )
21 |
22 | return (
23 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
54 | )
55 | }
56 |
57 | const $contentContainer: ViewStyle = {
58 | alignItems: "center",
59 | paddingHorizontal: spacing.large,
60 | paddingVertical: spacing.medium,
61 | flex: 1,
62 | }
63 |
64 | const $topSection: ViewStyle = {
65 | alignItems: "center",
66 | marginBottom: spacing.large,
67 | }
68 |
69 | const $heading: TextStyle = {
70 | marginBottom: spacing.medium,
71 | }
72 |
73 | const $errorSection: ViewStyle = {
74 | flex: 2,
75 | backgroundColor: colors.separator,
76 | marginBottom: spacing.medium,
77 | marginTop: spacing.large,
78 | borderRadius: 6,
79 | }
80 |
81 | const $errorSectionContentContainer: ViewStyle = {
82 | padding: spacing.medium,
83 | }
84 |
85 | const $errorBacktrace: TextStyle = {
86 | marginTop: spacing.medium,
87 | color: colors.textDim,
88 | }
89 |
90 | const $button: ViewStyle = {
91 | backgroundColor: colors.error,
92 | }
93 |
94 | const $resetButton: ViewStyle = {
95 | ...$button,
96 | paddingHorizontal: spacing.huge,
97 | }
98 |
99 | const $githubButton: ViewStyle = {
100 | ...$button,
101 | paddingHorizontal: spacing.huge,
102 | }
103 |
--------------------------------------------------------------------------------
/app/screens/InfoScreen/CodeOfConductScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { TextStyle, View, ViewStyle, Image, ImageStyle } from "react-native"
3 | import { Screen, Text } from "../../components"
4 | import { colors, spacing } from "../../theme"
5 | import { translate } from "../../i18n"
6 | import { openLinkInBrowser } from "../../utils/openLinkInBrowser"
7 | import { useAppNavigation, useHeader } from "../../hooks"
8 |
9 | const phoneNumber = "360-450-4752"
10 |
11 | const confImage = require("../../../assets/images/info-conf.jpg")
12 |
13 | export const CodeOfConductScreen = () => {
14 | const callPhoneNumber = () => openLinkInBrowser(`tel:${phoneNumber}`)
15 |
16 | const navigation = useAppNavigation()
17 |
18 | useHeader({
19 | title: translate("infoScreen.codeOfConductHeaderTitle"),
20 | leftIcon: "back",
21 | onLeftPress: () => navigation.goBack(),
22 | })
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {translate("infoScreen.reportingIncidentPart1")}
41 |
42 | {translate("infoScreen.reportingIncidentPart2")}
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | const $root: ViewStyle = {
50 | flex: 1,
51 | }
52 |
53 | const $content: ViewStyle = {
54 | paddingHorizontal: spacing.large,
55 | }
56 |
57 | const $image: ImageStyle = {
58 | marginVertical: spacing.large,
59 | width: "100%",
60 | borderRadius: 4,
61 | }
62 |
63 | const $mb: ViewStyle = {
64 | marginBottom: spacing.medium,
65 | }
66 |
67 | const $phoneNumber: TextStyle = {
68 | textDecorationLine: "underline",
69 | textDecorationColor: colors.text,
70 | }
71 |
72 | const $codeOfConductHeading: TextStyle = {
73 | marginBottom: spacing.extraSmall,
74 | }
75 |
--------------------------------------------------------------------------------
/app/screens/InfoScreen/ContactUsScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { TextStyle, View, ViewStyle } from "react-native"
3 | import { Screen, Text } from "../../components"
4 | import { translate } from "../../i18n"
5 | import { useAppNavigation, useHeader } from "../../hooks"
6 | import { colors, spacing } from "../../theme"
7 | import { openLinkInBrowser } from "../../utils/openLinkInBrowser"
8 |
9 | const phoneNumber = "360-450-4752"
10 | const emailAddress = "conf@infinite.red"
11 |
12 | export const ContactUsScreen = () => {
13 | const callPhoneNumber = () => openLinkInBrowser(`tel:${phoneNumber}`)
14 | const contactByEmail = () => openLinkInBrowser(`mailto:${emailAddress}`)
15 |
16 | const navigation = useAppNavigation()
17 |
18 | useHeader({
19 | title: translate("infoScreen.contactUsTitle"),
20 | leftIcon: "back",
21 | onLeftPress: () => navigation.goBack(),
22 | })
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | {translate("infoScreen.reachOut")}
30 |
31 |
32 |
33 |
34 |
35 | {translate("infoScreen.reportingIncidentPart1")}
36 |
37 | {translate("infoScreen.reportingIncidentPart2")}
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | const $root: ViewStyle = {
45 | flex: 1,
46 | }
47 |
48 | const $content: ViewStyle = {
49 | paddingHorizontal: spacing.large,
50 | }
51 |
52 | const $mb: TextStyle = {
53 | marginBottom: spacing.medium,
54 | }
55 |
56 | const $linkText: TextStyle = {
57 | textDecorationLine: "underline",
58 | textDecorationColor: colors.text,
59 | }
60 |
--------------------------------------------------------------------------------
/app/screens/InfoScreen/CreditsScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { TextStyle, View, ViewStyle, useWindowDimensions } from "react-native"
3 | import { Screen, Text } from "../../components"
4 | import { translate } from "../../i18n"
5 | import { useAppNavigation, useHeader } from "../../hooks"
6 | import { colors, spacing } from "../../theme"
7 | import { FlashList } from "@shopify/flash-list"
8 |
9 | type Person = {
10 | name: string
11 | role: string
12 | }
13 |
14 | const data: Person[] = [
15 | {
16 | name: "Gant Laborde",
17 | role: "Organizer of Chain React",
18 | },
19 | {
20 | name: "Justin Huskey",
21 | role: "Co-organizer of Chain React & App Design",
22 | },
23 | {
24 | name: "Jamon Holmgren",
25 | role: "Co-organizer of Chain React & App Development",
26 | },
27 | {
28 | name: "Jenna Fucci",
29 | role: "Designer (web, app, and print)",
30 | },
31 | {
32 | name: "Simran Sachdeva",
33 | role: "Workshop Assistant & App Testing",
34 | },
35 | {
36 | name: "Josh Yoes",
37 | role: "App Development",
38 | },
39 | {
40 | name: "Kate Kim",
41 | role: "App Development",
42 | },
43 | {
44 | name: "Robin Heinze",
45 | role: "App Lead & IR Table",
46 | },
47 | {
48 | name: "Mazen Chami",
49 | role: "App Development",
50 | },
51 | {
52 | name: "Frank Calise",
53 | role: "App Development",
54 | },
55 | {
56 | name: "Leon Kim",
57 | role: "App Development & Testing",
58 | },
59 | {
60 | name: "Silas Matson",
61 | role: "App Development",
62 | },
63 | {
64 | name: "Jon Major Condon",
65 | role: "App Development",
66 | },
67 | {
68 | name: "Yulian Glukhenko",
69 | role: "App Development",
70 | },
71 | {
72 | name: "Nick Morgan",
73 | role: "App Development",
74 | },
75 | {
76 | name: "Bryan Sterns",
77 | role: "App Testing",
78 | },
79 | {
80 | name: "Derek Greenberg",
81 | role: "App Testing",
82 | },
83 | {
84 | name: "Dan Edwards",
85 | role: "App Testing",
86 | },
87 | {
88 | name: "Jed Bartausky",
89 | role: "App Testing",
90 | },
91 | {
92 | name: "Ellie Croce",
93 | role: "App Testing",
94 | },
95 | {
96 | name: "Trevor Coleman",
97 | role: "App Testing",
98 | },
99 | ]
100 |
101 | export const CreditsScreen = () => {
102 | const navigation = useAppNavigation()
103 | const { width } = useWindowDimensions()
104 |
105 | useHeader({
106 | title: translate("infoScreen.creditsTitle"),
107 | leftIcon: "back",
108 | onLeftPress: () => navigation.goBack(),
109 | })
110 |
111 | return (
112 |
113 |
114 |
117 | }
118 | data={data}
119 | renderItem={({ item }) => {
120 | return (
121 |
122 |
123 |
124 |
125 | )
126 | }}
127 | estimatedItemSize={61}
128 | />
129 |
130 |
131 | )
132 | }
133 |
134 | const $root: ViewStyle = {
135 | alignItems: "center",
136 | flex: 1,
137 | }
138 |
139 | const $container: ViewStyle = {
140 | flex: 1,
141 | }
142 |
143 | const $heading: TextStyle = {
144 | paddingHorizontal: spacing.large,
145 | marginBottom: spacing.large,
146 | }
147 |
148 | const $person: TextStyle = {
149 | paddingHorizontal: spacing.large,
150 | marginVertical: spacing.small,
151 | }
152 |
153 | const $role: TextStyle = {
154 | marginTop: spacing.tiny,
155 | color: colors.palette.primary500,
156 | }
157 |
--------------------------------------------------------------------------------
/app/screens/InfoScreen/InfoScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { ViewStyle, ImageSourcePropType, ImageStyle, Pressable } from "react-native"
3 | import { AutoImage, ButtonLink, Carousel, Screen, Text } from "../../components"
4 | import { useAppNavigation, useHeader } from "../../hooks"
5 | import { colors, spacing } from "../../theme"
6 | import { translate } from "../../i18n"
7 | import { InfoStackParamList, InfoStackScreenProps } from "../../navigators/InfoStackNavigator"
8 | import { openLinkInBrowser } from "../../utils/openLinkInBrowser"
9 |
10 | const irImage1 = require("../../../assets/images/info-ir1.png")
11 | const irImage2 = require("../../../assets/images/info-ir2.png")
12 | const irImage3 = require("../../../assets/images/info-ir3.png")
13 |
14 | const carouselData: ImageSourcePropType[] = [irImage1, irImage2, irImage3]
15 |
16 | type Links = Array<{
17 | title: string
18 | link: keyof InfoStackParamList
19 | }>
20 |
21 | const links: Links = [
22 | {
23 | title: translate("infoScreen.codeOfConductHeaderTitle"),
24 | link: "CodeOfConduct",
25 | },
26 | {
27 | title: translate("infoScreen.contactUsTitle"),
28 | link: "ContactUs",
29 | },
30 | {
31 | title: translate("infoScreen.ourSponsorsTitle"),
32 | link: "OurSponsors",
33 | },
34 | {
35 | title: translate("infoScreen.creditsTitle"),
36 | link: "Credits",
37 | },
38 | ]
39 |
40 | export const InfoScreen: React.FunctionComponent> = () => {
41 | const mainNavigation = useAppNavigation()
42 | const infoStackNavigation = useAppNavigation()
43 |
44 | useHeader({
45 | title: translate("infoScreen.title"),
46 | leftText: " ",
47 | onLeftPress: () => mainNavigation.navigate("Debug"),
48 | })
49 |
50 | return (
51 |
52 |
53 |
54 |
60 |
61 | openLinkInBrowser("https://infinite.red/")} style={$buttonLink}>
62 | {translate("infoScreen.moreAboutIR")}
63 |
64 |
65 | {links.map((link, index) => (
66 | infoStackNavigation.navigate(link.link)}
69 | style={[$linksWrapper, index === links.length - 1 ? {} : $linksBorder]}
70 | >
71 |
72 |
73 |
74 | ))}
75 |
76 | )
77 | }
78 |
79 | const $root: ViewStyle = {
80 | flex: 1,
81 | }
82 |
83 | const $screenHeading: ViewStyle = {
84 | marginBottom: spacing.medium,
85 | paddingHorizontal: spacing.large,
86 | }
87 |
88 | const $linksWrapper: ViewStyle = {
89 | alignItems: "center",
90 | flexDirection: "row",
91 | justifyContent: "space-between",
92 | paddingEnd: spacing.extraLarge,
93 | paddingStart: spacing.large,
94 | paddingVertical: spacing.large,
95 | }
96 |
97 | const $linksBorder: ViewStyle = {
98 | borderColor: colors.separator,
99 | borderBottomWidth: 1,
100 | }
101 |
102 | const $arrow: ImageStyle = {
103 | height: 16,
104 | marginStart: spacing.medium,
105 | tintColor: colors.palette.primary500,
106 | width: 8,
107 | }
108 |
109 | const $buttonLink: ViewStyle = {
110 | paddingHorizontal: spacing.large,
111 | paddingVertical: spacing.medium,
112 | }
113 |
--------------------------------------------------------------------------------
/app/screens/InfoScreen/OurSponsorsScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import {
3 | ActivityIndicator,
4 | SectionList,
5 | SectionListData,
6 | TextStyle,
7 | View,
8 | ViewStyle,
9 | } from "react-native"
10 | import { Screen, Text } from "../../components"
11 | import { translate } from "../../i18n"
12 | import { useAppNavigation, useHeader } from "../../hooks"
13 | import { useSponsors } from "../../services/api"
14 | import { RawSponsor } from "../../services/api/webflow-api.types"
15 | import { WEBFLOW_MAP } from "../../services/api/webflow-consts"
16 | import { colors, spacing } from "../../theme"
17 | import { SponsorCard } from "./SponsorCard"
18 | import { groupBy } from "../../utils/groupBy"
19 |
20 | const sponsorTiers = Object.values(WEBFLOW_MAP.sponsorTier)
21 | type Tiers = (typeof sponsorTiers)[number]
22 |
23 | const initialTiers = sponsorTiers.reduce>(
24 | (acc, tier) => ({ ...acc, [tier]: [] }),
25 | {} as Record,
26 | )
27 |
28 | const useSponsorsSections = (): {
29 | isLoading: boolean
30 | sections: Array>
31 | } => {
32 | const { data: sponsors = [], isLoading } = useSponsors()
33 | const rawTiers = groupBy("sponsor-tier")(sponsors)
34 | const tiers = Object.keys(rawTiers).reduce>(
35 | (acc, tier) => ({
36 | ...acc,
37 | [tier]: rawTiers[tier] ?? [],
38 | }),
39 | initialTiers,
40 | )
41 | return {
42 | isLoading,
43 | sections: [
44 | {
45 | data: [
46 | ...(["Platinum", "Gold"] as const).flatMap((tier) =>
47 | tiers[tier].map((sponsor) => ({
48 | sponsor: sponsor.name,
49 | tier,
50 | promoSummary: sponsor["promo-summary"],
51 | externalURL: sponsor["external-url"],
52 | logo: {
53 | uri: sponsor.logo.url,
54 | },
55 | })),
56 | ),
57 | ...(["Silver", "Bronze", "Other"] as const).map((tier) => ({
58 | tier,
59 | sponsorImages: tiers[tier].map((sponsor) => ({
60 | sponsor: sponsor.name,
61 | uri: sponsor.logo.url,
62 | externalURL: sponsor["external-url"],
63 | })),
64 | })),
65 | ],
66 | },
67 | ],
68 | }
69 | }
70 |
71 | export const OurSponsorsScreen = () => {
72 | const navigation = useAppNavigation()
73 |
74 | useHeader({
75 | title: translate("infoScreen.ourSponsorsTitle"),
76 | leftIcon: "back",
77 | onLeftPress: () => navigation.goBack(),
78 | })
79 |
80 | const { isLoading, sections } = useSponsorsSections()
81 |
82 | return (
83 |
84 | {isLoading && (
85 |
86 | )}
87 | {!isLoading && sections.length > 0 && (
88 | <>
89 |
96 | }
97 | showsVerticalScrollIndicator={false}
98 | stickySectionHeadersEnabled={false}
99 | sections={sections}
100 | renderItem={({ item }) => (
101 |
102 |
103 |
104 | )}
105 | />
106 | >
107 | )}
108 |
109 | )
110 | }
111 |
112 | const $root: ViewStyle = {
113 | alignItems: "center",
114 | flex: 1,
115 | }
116 |
117 | const $activityIndicator: ViewStyle = {
118 | flex: 1,
119 | }
120 |
121 | const $container: ViewStyle = {
122 | paddingHorizontal: spacing.large,
123 | }
124 |
125 | const $heading: TextStyle = {
126 | paddingHorizontal: spacing.large,
127 | }
128 |
--------------------------------------------------------------------------------
/app/screens/ScheduleScreen/ScheduleDay.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, RefObject } from "react"
2 | import { Schedule } from "./ScheduleScreen"
3 | import {
4 | RefreshControl,
5 | TextStyle,
6 | View,
7 | ViewStyle,
8 | ViewToken,
9 | useWindowDimensions,
10 | } from "react-native"
11 | import { colors, layout, spacing } from "../../theme"
12 | import { Text } from "../../components"
13 | import ScheduleCard, { ScheduleCardProps } from "./ScheduleCard"
14 | import { ContentStyle, FlashList } from "@shopify/flash-list"
15 | import { useSafeAreaInsets } from "react-native-safe-area-context"
16 |
17 | const BUTTON_HEIGHT = 48
18 | const ITEM_HEIGHT = 242
19 |
20 | interface ScheduleDayProps {
21 | eventIndex: number
22 | index: number
23 | isRefetching: boolean
24 | isConfOver: boolean
25 | onRefresh: () => void
26 | onViewableItemsChanged: ({ viewableItems }: { viewableItems: ViewToken[] }) => void
27 | schedule: Schedule
28 | scheduleIndex: number
29 | scheduleListRef: RefObject>
30 | }
31 |
32 | const ScheduleDay: FC = (props) => {
33 | const {
34 | eventIndex,
35 | index,
36 | isRefetching,
37 | isConfOver,
38 | onRefresh,
39 | onViewableItemsChanged,
40 | schedule,
41 | scheduleIndex,
42 | scheduleListRef,
43 | } = props
44 |
45 | const { height: screenHeight, width: screenWidth } = useWindowDimensions()
46 | const { bottom } = useSafeAreaInsets()
47 | const listHeight = screenHeight - bottom - layout.headerHeight - layout.tabBarHeight
48 |
49 | return (
50 |
51 | {schedule.bannerTx && (
52 |
53 |
54 |
55 | )}
56 |
57 |
58 | data={schedule.events}
59 | estimatedItemSize={ITEM_HEIGHT}
60 | estimatedListSize={{ height: listHeight, width: screenWidth }}
61 | // To achieve better performance, specify the type based on the item
62 | getItemType={(item) => item.variant}
63 | keyExtractor={(item) => item.id}
64 | onViewableItemsChanged={onViewableItemsChanged}
65 | ref={scheduleListRef}
66 | scrollEventThrottle={16}
67 | showsVerticalScrollIndicator={false}
68 | viewabilityConfig={{ itemVisiblePercentThreshold: 1, minimumViewTime: 100 }}
69 | contentContainerStyle={
70 | scheduleIndex === index && eventIndex !== 0 ? $list : $listWithoutButton
71 | }
72 | renderItem={({ item, index: itemIndex }) => (
73 |
74 |
82 |
83 | )}
84 | refreshControl={
85 |
90 | }
91 | />
92 |
93 |
94 | )
95 | }
96 |
97 | const $banner: ViewStyle = {
98 | backgroundColor: colors.palette.primary400,
99 | paddingHorizontal: spacing.large,
100 | paddingVertical: spacing.extraSmall,
101 | }
102 |
103 | const $bannerText: TextStyle = {
104 | color: colors.palette.neutral800,
105 | }
106 |
107 | const $container: ViewStyle = {
108 | flex: 1,
109 | }
110 |
111 | const $listContainer: ViewStyle = {
112 | flex: 1,
113 | paddingHorizontal: spacing.large,
114 | }
115 |
116 | const $list: ContentStyle = {
117 | paddingTop: BUTTON_HEIGHT + spacing.extraLarge + spacing.extraSmall,
118 | paddingBottom: BUTTON_HEIGHT + spacing.medium,
119 | }
120 |
121 | const $listWithoutButton: ContentStyle = {
122 | paddingTop: spacing.extraLarge,
123 | paddingBottom: BUTTON_HEIGHT + spacing.medium,
124 | }
125 |
126 | const $cardContainer: ViewStyle = {
127 | paddingBottom: spacing.large,
128 | }
129 |
130 | export default ScheduleDay
131 |
--------------------------------------------------------------------------------
/app/screens/ScheduleScreen/ScheduleDayPicker.tsx:
--------------------------------------------------------------------------------
1 | import { format, isSameDay } from "date-fns"
2 | import React, { FC, ForwardedRef, useCallback } from "react"
3 | import {
4 | LayoutChangeEvent,
5 | Pressable,
6 | PressableProps,
7 | TextStyle,
8 | View,
9 | ViewStyle,
10 | useWindowDimensions,
11 | } from "react-native"
12 | import Animated, {
13 | useAnimatedStyle,
14 | SharedValue,
15 | interpolate,
16 | interpolateColor,
17 | } from "react-native-reanimated"
18 | import { colors, spacing, typography } from "../../theme"
19 | import { reportCrash } from "../../utils/crashReporting"
20 |
21 | interface AnimatedDayButtonProps extends PressableProps {
22 | onPress: () => void
23 | index: number
24 | text: string
25 | scrollX: SharedValue
26 | inputRange: number[]
27 | scheduleDates: Date[]
28 | }
29 |
30 | const AnimatedDayButton = React.forwardRef(
31 | (props: AnimatedDayButtonProps, ref: ForwardedRef) => {
32 | const { onPress, index, text, scrollX, inputRange, scheduleDates, ...rest } = props
33 | const outputRange = scheduleDates.map((_, scheduleIndex) =>
34 | index === scheduleIndex ? colors.palette.neutral800 : colors.palette.neutral100,
35 | )
36 |
37 | const $animatedTextStyle = useAnimatedStyle(() => {
38 | const color = interpolateColor(scrollX.value, inputRange, outputRange)
39 |
40 | return { color }
41 | })
42 |
43 | return (
44 |
45 |
46 | {text}
47 |
48 |
49 | )
50 | },
51 | )
52 |
53 | AnimatedDayButton.displayName = "AnimatedDayButton"
54 |
55 | type ScheduleDayPickerProps = {
56 | scrollX: SharedValue
57 | onItemPress: (itemIndex: number) => void
58 | scheduleDates: Date[]
59 | selectedScheduleDate: Date
60 | }
61 |
62 | type Measurement = {
63 | x: number
64 | y: number
65 | width: number
66 | height: number
67 | }
68 |
69 | const initialMeasures = (size: number): Measurement[] =>
70 | Array(size).fill({ x: 0, y: 0, width: 0, height: 0 })
71 |
72 | export const ScheduleDayPicker: FC = ({
73 | scrollX,
74 | onItemPress,
75 | scheduleDates,
76 | selectedScheduleDate,
77 | }) => {
78 | const { width: screenWidth } = useWindowDimensions()
79 | const wrapperWidth = screenWidth - spacing.extraSmall * 2
80 | const widthSize = wrapperWidth / scheduleDates.length
81 | const selectedScheduleIndex = scheduleDates.findIndex((date: Date) =>
82 | isSameDay(date, selectedScheduleDate),
83 | )
84 | const itemRefs = scheduleDates.map((_) => React.createRef())
85 | const [measures, setMeasures] = React.useState(initialMeasures(itemRefs.length))
86 |
87 | const onLayout = useCallback(
88 | ({ target }: LayoutChangeEvent) => {
89 | const m: Measurement[] = measures
90 | itemRefs.forEach((itemRef, index) => {
91 | itemRef.current?.measureLayout(
92 | target,
93 | (x, y, width, height) => {
94 | m[index] = { x, y, width, height }
95 | },
96 | () => {
97 | m[index] = { x: 0, y: 0, width: 0, height: 0 }
98 | reportCrash("ScheduleDayPicker-unable to measureLayout")
99 | },
100 | )
101 | })
102 |
103 | setMeasures(m)
104 | },
105 | [itemRefs],
106 | )
107 |
108 | const inputRange = scheduleDates.map((_, index) => index * screenWidth)
109 |
110 | const $animatedLeftStyle = useAnimatedStyle(() => {
111 | const translateX = interpolate(
112 | scrollX.value,
113 | inputRange,
114 | measures.map((measure) => measure.x + (selectedScheduleIndex - spacing.micro)),
115 | )
116 |
117 | return {
118 | transform: [{ translateX }],
119 | width: widthSize,
120 | }
121 | }, [inputRange, scrollX, measures])
122 |
123 | return (
124 |
125 | {measures.length > 0 && }
126 | {scheduleDates.map((date, index) => (
127 | {
131 | onItemPress(index)
132 | }}
133 | text={format(date, "EE")}
134 | accessibilityLabel={format(date, "EEEE")}
135 | {...{ index, scrollX, inputRange, scheduleDates }}
136 | />
137 | ))}
138 |
139 | )
140 | }
141 |
142 | const $animatedViewStyle: ViewStyle = {
143 | backgroundColor: colors.palette.primary500,
144 | borderRadius: 100,
145 | height: "100%",
146 | position: "absolute",
147 | }
148 |
149 | const $buttonBaseStyle: ViewStyle = {
150 | backgroundColor: colors.palette.neutral700,
151 | borderColor: colors.palette.primary500,
152 | borderRadius: 100,
153 | minHeight: 48,
154 | }
155 |
156 | const $textStyle: TextStyle = {
157 | color: colors.palette.neutral100,
158 | fontFamily: typography.primary.medium,
159 | fontSize: 16,
160 | }
161 |
162 | const $wrapperStyle: ViewStyle[] = [
163 | $buttonBaseStyle,
164 | {
165 | borderWidth: 1,
166 | flexDirection: "row",
167 | position: "absolute",
168 | bottom: spacing.extraSmall,
169 | left: 0,
170 | marginHorizontal: spacing.extraSmall,
171 | right: 0,
172 | },
173 | ]
174 |
175 | const $buttonStyle: ViewStyle[] = [
176 | $buttonBaseStyle,
177 | {
178 | alignItems: "center",
179 | backgroundColor: "transparent",
180 | flex: 1,
181 | justifyContent: "center",
182 | },
183 | ]
184 |
--------------------------------------------------------------------------------
/app/screens/TalkDetailsScreen/AssistantsList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { ImageStyle, TextStyle, View, ViewStyle } from "react-native"
3 | import { AutoImage, SocialButtons, Text } from "../../components"
4 | import { translate } from "../../i18n"
5 | import { colors, spacing } from "../../theme"
6 | import { Speaker } from "../../services/api/webflow-api.types"
7 | export interface AssistantsListProp {
8 | assistants: Speaker[]
9 | }
10 |
11 | export function AssistantsList(props: AssistantsListProp) {
12 | const { assistants } = props
13 |
14 | if (!assistants) return null
15 |
16 | return (
17 |
18 |
23 |
24 | {assistants.map((assistant) => (
25 |
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 | ))}
40 |
41 |
42 | )
43 | }
44 |
45 | const $assistantContainer: ViewStyle = {
46 | flexDirection: "row",
47 | flexWrap: "wrap",
48 | justifyContent: "space-between",
49 | }
50 |
51 | const $assistantRoot: ViewStyle = {
52 | marginTop: spacing.large,
53 | marginBottom: spacing.huge,
54 | }
55 |
56 | const $assistant: ViewStyle = {
57 | alignItems: "center",
58 | marginTop: spacing.large,
59 | width: "48%",
60 | justifyContent: "center",
61 | }
62 |
63 | const $assistantHeading: TextStyle = {
64 | marginTop: spacing.large,
65 | }
66 |
67 | const $assistantImage: ImageStyle = {
68 | height: 90,
69 | width: 90,
70 | aspectRatio: 1,
71 | borderRadius: 100,
72 | marginBottom: spacing.large,
73 | }
74 |
75 | const $assistantCompany: TextStyle = {
76 | marginTop: spacing.tiny,
77 | color: colors.palette.primary500,
78 | textTransform: "uppercase",
79 | }
80 |
81 | const $assistantLinks: ViewStyle = {
82 | flexDirection: "row",
83 | marginTop: spacing.large,
84 | }
85 |
--------------------------------------------------------------------------------
/app/screens/TalkDetailsScreen/TalkDetailsHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { TextStyle, View, ViewStyle } from "react-native"
3 | import Animated, { interpolate, SharedValue, useAnimatedStyle } from "react-native-reanimated"
4 | import { ClosedBanner, Text } from "../../components"
5 | import { BackButton } from "../../navigators/BackButton"
6 | import { colors, layout, spacing } from "../../theme"
7 |
8 | interface TalkDetailsHeaderProps {
9 | /**
10 | * Title of workshop/talk
11 | */
12 | title?: string
13 | /**
14 | * Workshop location or talk date/time
15 | */
16 | subtitle?: string
17 | /**
18 | * The Y position from the
19 | */
20 | scrollY: SharedValue
21 | /**
22 | * The container height from the details page
23 | * title + spacing + subtitle
24 | */
25 | headingHeight: number
26 | }
27 |
28 | const AnimatedText = Animated.createAnimatedComponent(Text)
29 |
30 | export const TalkDetailsHeader: React.FunctionComponent =
31 | function TalkDetailsHeader({ title, scrollY, headingHeight }) {
32 | const $animatedTitle = useAnimatedStyle(() => {
33 | const opacity = interpolate(scrollY.value, [headingHeight * 0.5, headingHeight], [0, 1])
34 |
35 | return { opacity }
36 | }, [headingHeight])
37 |
38 | return (
39 | <>
40 |
41 |
42 |
43 |
48 | {title}
49 |
50 |
51 | >
52 | )
53 | }
54 |
55 | const $rowContainer: ViewStyle = {
56 | height: layout.headerHeight,
57 | flexDirection: "row",
58 | alignItems: "center",
59 | justifyContent: "space-between",
60 | }
61 |
62 | const $centerTitle: TextStyle = {
63 | position: "absolute",
64 | width: "100%",
65 | textAlign: "center",
66 | paddingHorizontal: spacing.huge,
67 | zIndex: 1,
68 | color: colors.text,
69 | }
70 |
--------------------------------------------------------------------------------
/app/screens/TalkDetailsScreen/WorkshopDetailsScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react"
2 | import { ViewStyle, View, TextStyle } from "react-native"
3 | import { StackScreenProps } from "@react-navigation/stack"
4 | import { AppStackParamList } from "../../navigators"
5 | import { DynamicCarouselItem, Text, Screen, Carousel } from "../../components"
6 | import { colors, layout, spacing } from "../../theme"
7 | import { TalkDetailsHeader } from "./TalkDetailsHeader"
8 | import Animated from "react-native-reanimated"
9 | import { useSafeAreaInsets } from "react-native-safe-area-context"
10 | import { useScheduledEventsData } from "../../services/api"
11 | import { formatDate } from "../../utils/formatDate"
12 | import { AssistantsList } from "./AssistantsList"
13 | import { useScrollY } from "../../hooks"
14 |
15 | export const WorkshopDetailsScreen: FC> = ({
16 | route: { params },
17 | }) => {
18 | const [headingHeight, setHeadingHeight] = React.useState(0)
19 |
20 | const { scrollY, scrollHandler } = useScrollY()
21 | const { bottom: paddingBottom } = useSafeAreaInsets()
22 |
23 | const { data: scheduleData } = useScheduledEventsData()
24 | const schedule = scheduleData?.find((s) => s._id === params?.scheduleId)
25 |
26 | if (!schedule) return null
27 |
28 | const title = schedule.workshop?.name ?? ""
29 | const subtitle = `${formatDate(schedule["day-time"], "MMMM dd, h:mmaaa")} PT`
30 | const description = schedule.workshop?.abstract
31 | const instructors = schedule.workshop?.["instructor-s-2"]
32 | const assistants = schedule.workshop?.assistants
33 |
34 | const carouselData = instructors?.map((speaker) => ({
35 | image: { uri: speaker["speaker-photo"]?.url },
36 | imageStyle: { height: 320 },
37 | subtitle: speaker.name,
38 | label: speaker.company,
39 | body: speaker["speaker-bio"],
40 | socialButtons: [
41 | { url: speaker.twitter, icon: "twitter" },
42 | { url: speaker.github, icon: "github" },
43 | { url: speaker.externalURL, icon: "link" },
44 | ],
45 | })) as DynamicCarouselItem[]
46 |
47 | // store the translated strings in a const to avoid jest error
48 | const instructor = "workshopDetailsScreen.instructor"
49 |
50 | return (
51 |
52 |
53 |
54 |
60 |
61 |
62 | {
71 | setHeadingHeight(height)
72 | }}
73 | />
74 |
75 |
76 |
77 |
78 |
83 |
84 |
89 |
90 |
91 |
92 | {assistants?.length && }
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
100 | const $root: ViewStyle = {
101 | backgroundColor: colors.background,
102 | }
103 |
104 | const $scrollView: ViewStyle = {
105 | marginBottom: layout.headerHeight,
106 | }
107 |
108 | const $container = {
109 | paddingBottom: spacing.large,
110 | }
111 |
112 | const $containerSpacing: ViewStyle = {
113 | marginBottom: spacing.large,
114 | }
115 |
116 | const $contentSpacing: ViewStyle = {
117 | paddingHorizontal: spacing.large,
118 | }
119 |
120 | const $speakerPanelTitle: TextStyle = {
121 | ...$containerSpacing,
122 | marginTop: spacing.medium,
123 | }
124 |
125 | const $speakerPanelDescription: TextStyle = {
126 | marginBottom: spacing.huge,
127 | }
128 |
129 | const $title: TextStyle = {
130 | marginBottom: spacing.extraSmall,
131 | }
132 |
133 | const $subtitle: TextStyle = {
134 | color: colors.palette.primary500,
135 | }
136 |
137 | const $headingContainer: ViewStyle = {
138 | ...$contentSpacing,
139 | marginBottom: spacing.extraLarge,
140 | }
141 |
--------------------------------------------------------------------------------
/app/screens/VenuesScreen/VenuesScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react"
2 | import {
3 | ActivityIndicator,
4 | ImageSourcePropType,
5 | SectionList,
6 | SectionListData,
7 | ViewStyle,
8 | } from "react-native"
9 | import { Carousel, Screen } from "../../components"
10 | import { TabScreenProps } from "../../navigators/TabNavigator"
11 | import { colors } from "../../theme"
12 | import { useHeader } from "../../hooks"
13 | import { translate } from "../../i18n"
14 | import { useVenues } from "../../services/api"
15 | import { WEBFLOW_MAP } from "../../services/api/webflow-consts"
16 | import { customSort } from "../../utils/customSort"
17 | import { RawVenue } from "../../services/api/webflow-api.types"
18 |
19 | interface VenuesSection {
20 | body: string
21 | cta: {
22 | text: string
23 | link: string
24 | }
25 | imageSource: Array
26 | subtitle: string
27 | title: string
28 | }
29 |
30 | const getVenueDate = (venueTag: RawVenue["tag"]): string => {
31 | switch (WEBFLOW_MAP.venueTag[venueTag]) {
32 | case "Workshop":
33 | return "May 17"
34 | case "After Party":
35 | return "May 18"
36 | default:
37 | return "May 18-19"
38 | }
39 | }
40 |
41 | const useVenuesSections = (): {
42 | isLoading: boolean
43 | sections: Array>>
44 | } => {
45 | const { data: venues = [], isLoading: venuesLoading } = useVenues()
46 | const sortedVenues = customSort(venues, "slug", [
47 | "the-armory",
48 | "after-party-expensify-office",
49 | "courtyard-portland-city-center",
50 | ])
51 |
52 | return {
53 | isLoading: venuesLoading,
54 | sections: [
55 | {
56 | data: [
57 | sortedVenues?.map((venue) => ({
58 | imageSource: venue["venue-image-s"].map((image) => ({ uri: image.url })),
59 | title: venue.name,
60 | subtitle: `${WEBFLOW_MAP.venueTag[venue.tag]} • ${getVenueDate(venue.tag)}`,
61 | body: `${venue["street-address"]}\n${venue["city-state-zip"]}`,
62 | cta: {
63 | text: translate("venuesScreen.openInMaps"),
64 | link: `${venue["street-address"]},${venue["city-state-zip"]}`,
65 | },
66 | })),
67 | ],
68 | },
69 | ],
70 | }
71 | }
72 |
73 | const VenueCard = ({ body, cta, imageSource, subtitle, title }: VenuesSection) => (
74 |
83 | )
84 |
85 | export const VenuesScreen: FC> = () => {
86 | useHeader({ title: translate("venuesScreen.title") })
87 |
88 | const { sections, isLoading } = useVenuesSections()
89 |
90 | return (
91 |
92 | {isLoading && (
93 |
94 | )}
95 | {!isLoading && sections.length > 0 && (
96 | (
101 | <>
102 | {item.map((venue, index) => (
103 |
104 | ))}
105 | >
106 | )}
107 | />
108 | )}
109 |
110 | )
111 | }
112 |
113 | const $root: ViewStyle = {
114 | flex: 1,
115 | alignItems: "center",
116 | }
117 |
118 | const $activityIndicator: ViewStyle = {
119 | flex: 1,
120 | }
121 |
--------------------------------------------------------------------------------
/app/screens/WelcomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect } from "react"
2 | import {
3 | Image,
4 | ImageStyle,
5 | Platform,
6 | TextStyle,
7 | View,
8 | ViewStyle,
9 | useWindowDimensions,
10 | } from "react-native"
11 | import { Button, ClosedBanner, Screen, Text } from "../components"
12 | import { useAppNavigation } from "../hooks"
13 | import { AppStackScreenProps } from "../navigators"
14 | import { colors, spacing } from "../theme"
15 | import { prefetchScheduledEvents } from "../services/api"
16 |
17 | const welcomeLogo = require("../../assets/images/welcome-shapes.png")
18 |
19 | interface WelcomeScreenProps extends AppStackScreenProps<"Welcome"> {}
20 |
21 | export const WelcomeScreen: React.FC = (_props) => {
22 | const navigation = useAppNavigation()
23 | const { width } = useWindowDimensions()
24 |
25 | function goNext() {
26 | navigation.navigate("Tabs", { screen: "Schedule" })
27 | }
28 |
29 | useLayoutEffect(() => {
30 | prefetchScheduledEvents()
31 | }, [])
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
47 |
53 |
54 |
59 |
60 |
61 |
62 |
68 |
74 |
75 |
76 |
77 | )
78 | }
79 |
80 | const $container: ViewStyle = {
81 | flex: 1,
82 | backgroundColor: colors.background,
83 | }
84 |
85 | const $topContainer: ViewStyle = {
86 | flexShrink: 1,
87 | flexGrow: 1,
88 | flexBasis: "20%",
89 | justifyContent: "flex-start",
90 | }
91 |
92 | const $middleContainer: ViewStyle = {
93 | flexShrink: 1,
94 | flexGrow: 1,
95 | flexBasis: "50%",
96 | justifyContent: "center",
97 | paddingHorizontal: spacing.large,
98 | }
99 |
100 | const $bottomContainer: ViewStyle = {
101 | flexShrink: 1,
102 | flexGrow: 0,
103 | flexBasis: "20%",
104 | backgroundColor: colors.background,
105 | }
106 |
107 | const $bottomContentContainer: ViewStyle = {
108 | flex: 1,
109 | paddingBottom: spacing.large,
110 | ...Platform.select({
111 | ios: {
112 | justifyContent: "flex-end",
113 | } as ViewStyle,
114 | android: {
115 | justifyContent: "center",
116 | alignItems: "center",
117 | } as ViewStyle,
118 | default: {} as ViewStyle,
119 | }),
120 | }
121 |
122 | const $welcomeLogo: ImageStyle = {
123 | width: "100%",
124 | marginBottom: spacing.huge,
125 | }
126 |
127 | const $welcomeHeading: TextStyle = {
128 | marginBottom: spacing.large,
129 | }
130 |
131 | const $topBlurb: TextStyle = {
132 | marginBottom: spacing.large,
133 | }
134 |
--------------------------------------------------------------------------------
/app/screens/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./DebugScreen"
2 | export * from "./ErrorScreen/ErrorBoundary"
3 | export * from "./ExploreScreen/ExploreScreen"
4 | export * from "./InfoScreen/InfoScreen"
5 | export * from "./ChatScreen/ChatScreen"
6 | export * from "./ScheduleScreen/ScheduleScreen"
7 | export * from "./TalkDetailsScreen/TalkDetailsScreen"
8 | export * from "./TalkDetailsScreen/WorkshopDetailsScreen"
9 | export * from "./VenuesScreen/VenuesScreen"
10 | export * from "./WelcomeScreen"
11 |
--------------------------------------------------------------------------------
/app/services/api/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 | import { reportCrash } from "../../utils/crashReporting"
3 |
4 | interface PaginatedData {
5 | count: number
6 | limit: number
7 | offset: number
8 | total: number
9 | }
10 |
11 | export type PaginatedItems = PaginatedData & {
12 | items: T[]
13 | }
14 |
15 | export const axiosInstance = axios.create({
16 | baseURL: "https://chain-react-ai-chat.vercel.app/api/schedule/",
17 | // baseURL: "http://localhost:3000/api/schedule/",
18 | headers: {
19 | "Content-Type": "application/json",
20 | "User-Agent": "Webflow Javascript SDK / 1.0",
21 | },
22 | })
23 |
24 | axiosInstance.interceptors.response.use(
25 | (response) => {
26 | return response
27 | },
28 | (error) => {
29 | reportCrash(error)
30 | return Promise.reject(error)
31 | },
32 | )
33 |
34 | axiosInstance.interceptors.request.use(
35 | (request) => request,
36 | (error) => {
37 | reportCrash(error)
38 | return Promise.reject(error)
39 | },
40 | )
41 |
--------------------------------------------------------------------------------
/app/services/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./webflow-api"
2 |
--------------------------------------------------------------------------------
/app/services/api/react-query.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from "@tanstack/react-query"
2 |
3 | // Creating a react-query client with the stale time set to "Infinity" so that its never stale
4 | export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity } } })
5 |
--------------------------------------------------------------------------------
/app/services/api/webflow-api.ts:
--------------------------------------------------------------------------------
1 | import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query"
2 | import { Schedule } from "../../screens"
3 | import { axiosInstance, PaginatedItems } from "./axios"
4 | import type {
5 | RawRecommendations,
6 | RawRecurringEvents,
7 | RawScheduledEvent,
8 | RawSpeaker,
9 | RawSponsor,
10 | RawTalk,
11 | RawVenue,
12 | RawWorkshop,
13 | } from "./webflow-api.types"
14 | import {
15 | CollectionConst,
16 | RECOMMENDATIONS,
17 | RECURRING_EVENTS,
18 | SCHEDULE,
19 | SPEAKERS,
20 | SPONSORS,
21 | TALKS,
22 | VENUES,
23 | WORKSHOPS,
24 | } from "./webflow-consts"
25 | import {
26 | cleanedSchedule,
27 | cleanedSpeakers,
28 | cleanedSponsors,
29 | cleanedTalks,
30 | cleanedWorkshops,
31 | convertScheduleToScheduleCard,
32 | } from "./webflow-helpers"
33 | import { queryClient } from "./react-query"
34 | import webflowData from "./webflow-data"
35 |
36 | // This function isn't currently being used but it's the one we used to use to fetch data from Webflow
37 | const _getCollectionById = async (collectionId: string) => {
38 | const { data } = await axiosInstance.get>(`/collections/${collectionId}/items`)
39 | return data.items
40 | }
41 |
42 | const getLocalCollectionById = async (collectionId: string) => {
43 | return (webflowData as unknown as Record>)[collectionId]
44 | }
45 |
46 | const webflowOptions = (
47 | collection: Collection,
48 | ) =>
49 | ({
50 | queryKey: [collection.key, collection.collectionId] as const,
51 | queryFn: async () => getLocalCollectionById(collection.collectionId),
52 | } satisfies UseQueryOptions)
53 |
54 | const recommendationsOptions = webflowOptions(RECOMMENDATIONS)
55 | export const useRecommendations = () => useQuery(recommendationsOptions)
56 |
57 | const sponsorsOptions = webflowOptions(SPONSORS)
58 | export const useSponsors = () => {
59 | const { data: sponsors, ...rest } = useQuery(sponsorsOptions)
60 | const data = cleanedSponsors(sponsors)
61 |
62 | return { data, ...rest }
63 | }
64 |
65 | const venuesOptions = webflowOptions(VENUES)
66 | export const useVenues = () => useQuery(venuesOptions)
67 |
68 | const speakersOptions = webflowOptions(SPEAKERS)
69 | const workshopsOptions = webflowOptions(WORKSHOPS)
70 | const recurringEventsOptions = webflowOptions(RECURRING_EVENTS)
71 | const talksOptions = webflowOptions(TALKS)
72 | const scheduledEventsOptions = webflowOptions(SCHEDULE)
73 |
74 | const scheduledEventQueries = [
75 | speakersOptions,
76 | workshopsOptions,
77 | recurringEventsOptions,
78 | talksOptions,
79 | scheduledEventsOptions,
80 | ] as const
81 |
82 | export const prefetchScheduledEvents = async () => {
83 | scheduledEventQueries.forEach(async (query) => {
84 | await queryClient.prefetchQuery(query as UseQueryOptions)
85 | })
86 | }
87 |
88 | export const useScheduledEventsData = () => {
89 | const queries = useQueries({
90 | queries: scheduledEventQueries,
91 | })
92 |
93 | const isLoading = queries.map((query) => query.isLoading).some((isLoading) => isLoading)
94 | const isRefetching = queries
95 | .map((query) => query.isRefetching)
96 | .some((isRefetching) => isRefetching)
97 | const refetch = async () => Promise.all(queries.map((query) => query.refetch()))
98 |
99 | const [
100 | { data: speakers },
101 | { data: workshops },
102 | { data: recurringEvents },
103 | { data: talks },
104 | { data: scheduledEvents },
105 | ] = queries
106 |
107 | return {
108 | data: isLoading
109 | ? []
110 | : cleanedSchedule({
111 | recurringEvents,
112 | scheduledEvents,
113 | speakers: cleanedSpeakers(speakers),
114 | talks: cleanedTalks({ speakers, talks }),
115 | workshops: cleanedWorkshops(workshops, cleanedSpeakers(speakers)),
116 | }),
117 | isLoading,
118 | isRefetching,
119 | refetch,
120 | }
121 | }
122 |
123 | export const useScheduleScreenData = () => {
124 | const { data: events, isLoading, isRefetching, refetch } = useScheduledEventsData()
125 |
126 | return {
127 | isLoading,
128 | isRefetching,
129 | schedules: [
130 | {
131 | bannerTx: "scheduleScreen.workshopBanner",
132 | date: "2023-05-17",
133 | title: "React Native Workshops",
134 | events: convertScheduleToScheduleCard(events, "Wednesday"),
135 | },
136 | {
137 | date: "2023-05-18",
138 | title: "Conference Day 1",
139 | events: convertScheduleToScheduleCard(events, "Thursday"),
140 | },
141 | {
142 | date: "2023-05-19",
143 | title: "Conference Day 2",
144 | events: convertScheduleToScheduleCard(events, "Friday"),
145 | },
146 | ] satisfies Schedule[],
147 | refetch,
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/app/services/api/webflow-consts.ts:
--------------------------------------------------------------------------------
1 | export const SITE_ID = "5ca38f35db5d2ea94aea469d"
2 |
3 | export const SPONSORS = {
4 | collectionId: "640a728fc24f8e73575fe189",
5 | key: "sponsors",
6 | } as const
7 |
8 | export const SPEAKERS = {
9 | collectionId: "640a728fc24f8e94385fe188",
10 | key: "speakers",
11 | } as const
12 |
13 | export const SPEAKER_NAMES = {
14 | collectionId: "640a728fc24f8e74d05fe18a",
15 | key: "speakerNames",
16 | }
17 |
18 | export const WORKSHOPS = {
19 | collectionId: "640a728fc24f8e7f635fe187",
20 | key: "workshops",
21 | }
22 |
23 | export const SCHEDULE = {
24 | collectionId: "640a728fc24f8e63325fe185",
25 | key: "schedule",
26 | } as const
27 |
28 | export const PAST_TALKS = {
29 | collectionId: "640a728fc24f8e76ef5fe186",
30 | key: "pastTalks",
31 | } as const
32 |
33 | export const RECURRING_EVENTS = {
34 | collectionId: "640a728fc24f8e85a75fe18c",
35 | key: "recurringEvents",
36 | } as const
37 |
38 | export const TALKS = {
39 | collectionId: "640a728fc24f8e31ee5fe18e",
40 | key: "talks",
41 | } as const
42 |
43 | export const VENUES = {
44 | collectionId: "640a728fc24f8e553c5fe18d",
45 | key: "venues",
46 | } as const
47 |
48 | export const RECOMMENDATIONS = {
49 | collectionId: "640a728fc24f8e083b5fe18f",
50 | key: "recommendations",
51 | } as const
52 |
53 | export type CollectionConst =
54 | | typeof SPONSORS
55 | | typeof SPEAKERS
56 | | typeof SPEAKER_NAMES
57 | | typeof WORKSHOPS
58 | | typeof SCHEDULE
59 | | typeof PAST_TALKS
60 | | typeof RECURRING_EVENTS
61 | | typeof TALKS
62 | | typeof VENUES
63 | | typeof RECOMMENDATIONS
64 |
65 | // [NOTE] these keys probably have to change when webflow is updated
66 | // `/collections/${collectionId}` api will the keys
67 | export const WEBFLOW_MAP = {
68 | workshopLevel: {
69 | bcb33aac3cd85ef6f2e7a97cf23c9771: "Beginner",
70 | e9d1df0d23f4049bd9d1a6fe83c5db01: "Intermediate",
71 | "860319fadc9cd03654561fba21490285": "Advanced",
72 | "9ec823420253bd29f312b005681510ac": "Beginner-Intermediate",
73 | },
74 | workshopType: {
75 | d9770c43cd59f01f2d60b288d65c1f90: "New",
76 | c8af4236d64f5c25c09e61e4633badb0: "Top Seller",
77 | },
78 | workshopYear: {
79 | "4f94582394abff4ed4dec0f1a27abf32": "2023",
80 | "96a6b94b94afb3a981dddbfab1e32a31": "2019",
81 | "0b3b05a9f43de273fc88d5b74aa4596e": "2018",
82 | "64ee2179ea49572e201495acdcd77453": "2017",
83 | },
84 | speakersType: {
85 | "97dae28f90a767132ee88e80a8537af8": "Speaker",
86 | "079e51435c82a91426f9c3acc7b0343a": "Panelist",
87 | f23ef92d0cef6be6fd60654d54770c96: "Workshop",
88 | "07948ce9361d13f707fdb4e663cbe9a5": "Emcee",
89 | },
90 | speakersTalk: {
91 | "2f3097a3529a99ed4d688e9ce05034d6": "Beginner",
92 | "33984dd1db455114d65e3bd9989f4fad": "Intermediate",
93 | ce1ba34575f5a7e30ba9c4f3c33c8211: "Advanced",
94 | },
95 | scheduleDay: {
96 | "63ac4ade8b2d5a981780570e01bed34d": "Wednesday",
97 | ed2cfa99e27dce5d1a425a419f170eb3: "Thursday",
98 | "93f921892f42ef212e824c80e0db4da0": "Friday",
99 | },
100 | scheduleType: {
101 | "4206976061fcd6327bd12ce6aac856eb": "Talk",
102 | dd977a70188a93af399ad496d6cf2785: "Recurring",
103 | "8fc8810c6c61b7e3939280149fc5f84e": "Speaker Panel",
104 | "7ccbd551ac994b4489c4fe31ad985120": "Workshop",
105 | a46728e5ac2795216173113b4cd6d91a: "Sponsored",
106 | "67acc937d6af3a65b7b349a2bec4f701": "Lightning Talk",
107 | "123abc": "Trivia Show", // adding a dummy value for the trivia show
108 | },
109 | recurringEventType: {
110 | "63b8a530a5f15f0d26539f07": "Check-in & Registration",
111 | "63b8a5a76623c60c4e72db89": "Morning Break",
112 | "63b8a5e54f99644c1c457253": "Lunch Break",
113 | "63b8a62fcfc084793e4d5b60": "Afternoon Break",
114 | },
115 | talkType: {
116 | "38ba1361ae664a13e4a03f20ae153dc8": "Talk",
117 | "3aa9ece8012afed5d4e548180b2713e0": "Emcee",
118 | e66d50161e7027f9c8646ac4ec9c02a9: "Speaker Panel",
119 | },
120 | location: {
121 | "63b8a958a5f15f379953e0da": "Courtyard Portland City Center",
122 | "63b8a865e6b85e71b453fd3d": "The Armory",
123 | },
124 | venueTag: {
125 | cf3d319cdaa0e6787b03ab94e74e3b8e: "Workshop",
126 | d098b6fc74f6c55c6084ea42c385e7f6: "Conference",
127 | "6157eba78b067d800ea4cfd5188e782f": "After Party",
128 | },
129 | sponsorTier: {
130 | fe9c5b4d9fc3b0cf366607646cebcb95: "Platinum",
131 | "47d258ed5e5d2fe69d35dc685bd37fb9": "Gold",
132 | c1857a23dee35cbfbb7b694b89d16296: "Silver",
133 | "44bcdc97a36f741fe7e70aec6a33e936": "Bronze",
134 | "73853c318edcbdaebc8d603c9bc1b968": "Other",
135 | },
136 | recommendationType: {
137 | "3541dc4db3502b41c75043518060800d": "Food/Drink",
138 | a5028d71ed9c315a6e9fa67778f2579d: "SightSee",
139 | f42e3ac1a464004c28d91ddf3945b654: "Unique/to/Portland",
140 | },
141 | triviaShow: {
142 | title: "Trivia Show",
143 | variant: "trivia-show",
144 | },
145 | } as const
146 |
--------------------------------------------------------------------------------
/app/services/reactotron/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./reactotron"
2 |
--------------------------------------------------------------------------------
/app/services/reactotron/reactotronClient.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is loaded in React Native and exports the RN version
3 | * of Reactotron's client.
4 | *
5 | * Web is loaded from reactotronClient.web.ts.
6 | */
7 | import Reactotron from "reactotron-react-native"
8 | export { Reactotron }
9 |
--------------------------------------------------------------------------------
/app/services/reactotron/reactotronConfig.ts:
--------------------------------------------------------------------------------
1 | export interface ReactotronConfig {
2 | /** The name of the app. */
3 | name?: string
4 | /** The host to connect to: default 'localhost'. */
5 | host?: string
6 | /** Should we use async storage */
7 | useAsyncStorage?: boolean
8 | /** Should we clear Reactotron when load? */
9 | clearOnLoad?: boolean
10 | /** log the initial data that we put into the state on startup? */
11 | logInitialState?: boolean
12 | /** log snapshot changes. */
13 | logSnapshots?: boolean
14 | }
15 |
16 | /**
17 | * The default Reactotron configuration.
18 | */
19 | export const DEFAULT_REACTOTRON_CONFIG: ReactotronConfig = {
20 | clearOnLoad: true,
21 | host: "localhost",
22 | useAsyncStorage: true,
23 | logInitialState: true,
24 | logSnapshots: false,
25 | }
26 |
--------------------------------------------------------------------------------
/app/services/reactotron/reactotronFake.ts:
--------------------------------------------------------------------------------
1 | import { Reactotron } from "reactotron-core-client"
2 | import { ReactotronReactNative } from "reactotron-react-native"
3 |
4 | /** Do Nothing. */
5 | const noop = (): void => undefined
6 |
7 | /**
8 | * Fake no-op version of Reactotron, so nothing breaks if a console.tron.*
9 | * gets through our conditionals.
10 | */
11 | export const fakeReactotron: Reactotron & ReactotronReactNative = {
12 | benchmark: (_title: string) => ({
13 | step: (_startKey: string) => undefined,
14 | stop: (_stopKey: string) => undefined,
15 | last: noop,
16 | }),
17 | clear: noop,
18 | close: noop,
19 | configure: () => fakeReactotron,
20 | connect: () => fakeReactotron,
21 | display: noop,
22 | error: noop,
23 | image: noop,
24 | log: noop,
25 | logImportant: noop,
26 | onCustomCommand: () => noop,
27 | overlay: noop,
28 | reportError: noop,
29 | send: noop,
30 | startTimer: () => () => Date.now(),
31 | storybookSwitcher: (() => noop) as ReactotronReactNative["storybookSwitcher"],
32 | use: () => fakeReactotron,
33 | useReactNative: () => fakeReactotron,
34 | warn: noop,
35 | }
36 |
--------------------------------------------------------------------------------
/app/theme/colors.ts:
--------------------------------------------------------------------------------
1 | const palette = {
2 | neutral100: "#F8F7F7",
3 | neutral200: "#D8DCE1",
4 | neutral300: "#8C97A4",
5 | neutral400: "#394D64",
6 | neutral500: "#152B42",
7 | neutral600: "#102438",
8 | neutral700: "#081828",
9 | neutral800: "#060B10",
10 |
11 | primary100: "#E2E1F2",
12 | primary200: "#BFBCEB",
13 | primary300: "#9C96F8",
14 | primary400: "#8880FF",
15 | primary500: "#776EFB",
16 | primary600: "#655DE5",
17 | primary700: "#4E46C6",
18 | primary800: "#4039B5",
19 |
20 | secondary100: "#E5F4F3",
21 | secondary200: "#D0E9E7",
22 | secondary300: "#ACDDD9",
23 | secondary400: "#82D3CD",
24 | secondary500: "#00C6B7",
25 | secondary600: "#19BFB3",
26 | secondary700: "#4CB8B0",
27 | secondary800: "#3FA39B",
28 |
29 | bold100: "#FAE8E4",
30 | bold200: "#F2D2CB",
31 | bold300: "#F1A493",
32 | bold400: "#EE856E",
33 | bold500: "#F05F3F",
34 | bold600: "#E05C3F",
35 | bold700: "#CD4D31",
36 | bold800: "#BD4806",
37 |
38 | highlight100: "#FAF2E3",
39 | highlight200: "#FAE9C5",
40 | highlight300: "#FDE3AC",
41 | highlight400: "#FFD377",
42 | highlight500: "#FFC854",
43 | highlight600: "#F9C75F",
44 | highlight700: "#F2BA4D",
45 | highlight800: "#EDA943",
46 |
47 | angry100: "#F2D6CD",
48 | angry500: "#C03403",
49 | } as const
50 |
51 | export const colors = {
52 | /**
53 | * The palette is available to use, but prefer using the name.
54 | * This is only included for rare, one-off cases. Try to use
55 | * semantic names as much as possible.
56 | */
57 | palette,
58 | /**
59 | * A helper for making something see-thru.
60 | */
61 | transparent: "rgba(0, 0, 0, 0)",
62 | /**
63 | * The default text color in many components.
64 | */
65 | text: palette.neutral100,
66 | /**
67 | * Secondary text information.
68 | */
69 | textDim: palette.primary100,
70 | /**
71 | * The default color of the screen background.
72 | */
73 | background: palette.neutral700,
74 | /**
75 | * The default border color.
76 | */
77 | border: palette.primary500,
78 | /**
79 | * The main tinting color.
80 | */
81 | tint: palette.primary500,
82 | /**
83 | * A subtle color used for lines.
84 | */
85 | separator: palette.neutral400,
86 | /**
87 | * Error messages.
88 | */
89 | error: palette.angry500,
90 | /**
91 | * Error Background.
92 | *
93 | */
94 | errorBackground: palette.angry100,
95 | }
96 |
--------------------------------------------------------------------------------
/app/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./colors"
2 | export * from "./spacing"
3 | export * from "./typography"
4 | export * from "./timing"
5 |
--------------------------------------------------------------------------------
/app/theme/spacing.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Use these spacings for margins/paddings and other whitespace throughout your app.
3 | */
4 | export const spacing = {
5 | micro: 2,
6 | tiny: 4,
7 | extraSmall: 8,
8 | small: 12,
9 | medium: 16,
10 | large: 24,
11 | extraLarge: 32,
12 | huge: 48,
13 | massive: 64,
14 | } as const
15 |
16 | export const layout = {
17 | // Horizontal padding used for the app
18 | horizontalGutter: spacing.large,
19 | headerHeight: 56,
20 | tabBarHeight: 70,
21 | mediaButtonGutter: spacing.medium * 2 + 16 + spacing.small, // 16 for the font size, medium * 2 for the vertical padding, small for extra breathing room
22 | }
23 |
24 | export type Spacing = keyof typeof spacing
25 |
--------------------------------------------------------------------------------
/app/theme/timing.ts:
--------------------------------------------------------------------------------
1 | export const timing = {
2 | /**
3 | * The duration (ms) for quick animations.
4 | */
5 | quick: 300,
6 | }
7 |
--------------------------------------------------------------------------------
/app/theme/typography.ts:
--------------------------------------------------------------------------------
1 | export const customFontsToLoad = {
2 | gothamRoundedBold: require("../../assets/fonts/GothamRounded-Bold.otf"),
3 | gothamRoundedBook: require("../../assets/fonts/GothamRounded-Book.otf"),
4 | gothamRoundedMedium: require("../../assets/fonts/GothamRounded-Medium.otf"),
5 | gothamSsmBold: require("../../assets/fonts/GothamSSm-Bold.otf"),
6 | gothamSsmBook: require("../../assets/fonts/GothamSSm-Book.otf"),
7 | gothamSsmMedium: require("../../assets/fonts/GothamSSm-Medium.otf"),
8 | }
9 |
10 | const fonts = {
11 | gothamRounded: {
12 | book: "gothamRoundedBook",
13 | medium: "gothamRoundedMedium",
14 | bold: "gothamRoundedBold",
15 | },
16 | gothamSsm: {
17 | book: "gothamSsmBook",
18 | medium: "gothamSsmMedium",
19 | bold: "gothamSsmBold",
20 | },
21 | }
22 |
23 | export const typography = {
24 | /**
25 | * The fonts are available to use, but prefer using the semantic name.
26 | */
27 | fonts,
28 | /**
29 | * The primary font. Used in most places.
30 | */
31 | primary: fonts.gothamSsm,
32 | /**
33 | * An alternate font used for perhaps titles and stuff.
34 | */
35 | secondary: fonts.gothamRounded,
36 | }
37 |
--------------------------------------------------------------------------------
/app/utils/crashReporting.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * If you're using Crashlytics: https://rnfirebase.io/crashlytics/usage
3 | */
4 | import crashlytics from "@react-native-firebase/crashlytics"
5 |
6 | /**
7 | * Error classifications used to sort errors on error reporting services.
8 | */
9 | export enum ErrorType {
10 | /**
11 | * An error that would normally cause a red screen in dev
12 | * and force the user to sign out and restart.
13 | */
14 | FATAL = "Fatal",
15 | /**
16 | * An error caught by try/catch where defined using Reactotron.tron.error.
17 | */
18 | HANDLED = "Handled",
19 | }
20 |
21 | /**
22 | * Manually report a handled error.
23 | */
24 | export const reportCrash = (error: any, type: ErrorType = ErrorType.FATAL) => {
25 | if (__DEV__ || process.env.NODE_ENV === "development") {
26 | // Log to console and Reactotron in development
27 | const message = error.message || "Unknown"
28 | console.error(error)
29 | console.log(message, type)
30 | console.tron.log(error)
31 | } else {
32 | crashlytics().recordError(error)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/utils/customSort.ts:
--------------------------------------------------------------------------------
1 | interface SortableObject {
2 | [key: string]: any
3 | }
4 |
5 | /**
6 | *
7 | * @param arr—array of objects to sort
8 | * @param property—key of the object to sort by
9 | * @param order—array of keys to sort by
10 | * @returns sorted array
11 | *
12 | * @example
13 | * ```tsx
14 | * const sortedVenues = customSort(venues, "slug", [
15 | * "the-armory",
16 | * "after-party-expensify-office",
17 | * "courtyard-portland-city-center",
18 | * ])
19 | * ```
20 | * output:
21 | * ```tsx
22 | * sortedVenues = [
23 | * {}, // "the-armory"
24 | * {}, // "after-party-expensify-office"
25 | * {}, // "courtyard-portland-city-center"
26 | * ... // other venues
27 | * ]
28 | */
29 | export const customSort = (
30 | arr: T[],
31 | property: keyof T,
32 | order: T[keyof T][],
33 | ): T[] => {
34 | const orderMap: { [key: string]: number } = {}
35 | for (let i = 0; i < order.length; i++) {
36 | orderMap[order[i]] = i
37 | }
38 | return arr.sort((a, b) => {
39 | if (a[property] === undefined && b[property] === undefined) {
40 | return 0
41 | } else if (a[property] === undefined) {
42 | return 1
43 | } else if (b[property] === undefined) {
44 | return -1
45 | } else {
46 | const aOrder = orderMap[a[property]]
47 | const bOrder = orderMap[b[property]]
48 | return aOrder - bOrder
49 | }
50 | })
51 | }
52 |
53 | /**
54 | * Sorts an object's keys by a given order
55 | *
56 | * @param obj—object to sort
57 | * @param order—array of keys to sort by
58 | *
59 | * @example
60 | * ```tsx
61 | * const recommendations = customSortObjectKeys(
62 | * {
63 | * "Food/Drink": [],
64 | * "Unique/to/Portland": [],
65 | * "SightSee": []
66 | * },
67 | * ["SightSee", "Food/Drink", "Unique/to/Portland"]
68 | * )
69 | * ```
70 | * output:
71 | * ```tsx
72 | * recommendations = {
73 | * "SightSee": [],
74 | * "Food/Drink": [],
75 | * "Unique/to/Portland": []
76 | * }
77 | ```
78 | */
79 | export const customSortObjectKeys = >(
80 | obj: T,
81 | order: Array,
82 | ): T => {
83 | const sortedObj = {} as T
84 | for (const key of order) {
85 | if (Object.prototype.hasOwnProperty.call(obj, key)) {
86 | sortedObj[key] = obj[key]
87 | }
88 | }
89 | for (const key in obj) {
90 | if (!Object.prototype.hasOwnProperty.call(sortedObj, key)) {
91 | sortedObj[key] = obj[key]
92 | }
93 | }
94 | return sortedObj
95 | }
96 |
--------------------------------------------------------------------------------
/app/utils/delay.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A "modern" sleep statement.
3 | *
4 | * @param ms The number of milliseconds to wait.
5 | */
6 | export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
7 |
--------------------------------------------------------------------------------
/app/utils/formatDate.ts:
--------------------------------------------------------------------------------
1 | import { Locale, format, parseISO } from "date-fns"
2 | import I18n from "i18n-js"
3 |
4 | import ar from "date-fns/locale/ar-SA"
5 | import ko from "date-fns/locale/ko"
6 | import en from "date-fns/locale/en-US"
7 |
8 | type Options = Parameters[2]
9 |
10 | const getLocale = (): Locale => {
11 | const locale = I18n.currentLocale().split("-")[0]
12 | return locale === "ar" ? ar : locale === "ko" ? ko : en
13 | }
14 |
15 | export const parseDate = (date: string | Date) => (date instanceof Date ? date : parseISO(date))
16 |
17 | const parsedDateToPSTDate = (date: string | Date): Date => {
18 | const inputDate = parseDate(date)
19 | const timeZone = "America/Los_Angeles"
20 |
21 | const formatter = new Intl.DateTimeFormat("en-US", {
22 | timeZone,
23 | year: "numeric",
24 | month: "2-digit",
25 | day: "2-digit",
26 | hour: "2-digit",
27 | minute: "2-digit",
28 | second: "2-digit",
29 | hour12: false,
30 | })
31 |
32 | const parts = formatter.formatToParts(inputDate)
33 | const year = parseInt(parts.find((part) => part.type === "year")?.value || "", 10)
34 | const month = parseInt(parts.find((part) => part.type === "month")?.value || "", 10) - 1 // Months are 0-indexed in JavaScript
35 | const day = parseInt(parts.find((part) => part.type === "day")?.value || "", 10)
36 | const hour = parseInt(parts.find((part) => part.type === "hour")?.value || "", 10)
37 | const minute = parseInt(parts.find((part) => part.type === "minute")?.value || "", 10)
38 | const second = parseInt(parts.find((part) => part.type === "second")?.value || "", 10)
39 |
40 | return new Date(year, month, day, hour, minute, second)
41 | }
42 |
43 | export const formatDate = (date: string | Date, dateFormat?: string, options?: Options) => {
44 | const locale = getLocale()
45 | const dateOptions = {
46 | ...options,
47 | locale,
48 | }
49 | return format(parsedDateToPSTDate(date), dateFormat ?? "MMM dd, yyyy", dateOptions)
50 | }
51 |
52 | type DayTime = { ["day-time"]: Date | string }
53 | export const sortByTime = (a: DayTime, b: DayTime) =>
54 | parseDate(a["day-time"]).getTime() - parseDate(b["day-time"]).getTime()
55 |
--------------------------------------------------------------------------------
/app/utils/groupBy.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param key string—the key to group by
4 | * @returns an object with the grouped values, keyed by the value of the key
5 | *
6 | * @example
7 | * ```tsx
8 | * const grouped = groupBy('type')([
9 | * { type: "Food/Drink", name: 'Starbucks Reserve' },
10 | * { type: "Unique/to/Portland", name: 'Ground Kontrol Arcade' },
11 | * { type: "Food/Drink", name: 'Hotel Restaurant - The Original Dinerant' },
12 | * ])
13 | * ```
14 | * output:
15 | * ```tsx
16 | * grouped = {
17 | * "Food/Drink": [
18 | * { type: "Food/Drink", name: 'Starbucks Reserve' },
19 | * { type: "Food/Drink", name: 'Hotel Restaurant - The Original Dinerant' },
20 | * ],
21 | * "Unique/to/Portland": [
22 | * { type: "Unique/to/Portland", name: 'Ground Kontrol Arcade' },
23 | * ]
24 | * }
25 | * ```
26 | */
27 | export const groupBy =
28 | (key: string) =>
29 | (array: T[]) =>
30 | array.reduce>(
31 | (objectsByKeyValue, obj) => ({
32 | ...objectsByKeyValue,
33 | [obj[key as keyof T] as string]: (
34 | objectsByKeyValue[obj[key as keyof T] as string] || []
35 | ).concat(obj),
36 | }),
37 | {},
38 | )
39 |
--------------------------------------------------------------------------------
/app/utils/ignoreWarnings.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Ignore some yellowbox warnings. Some of these are for deprecated functions
3 | * that we haven't gotten around to replacing yet.
4 | */
5 | import { LogBox } from "react-native"
6 |
7 | // prettier-ignore
8 | LogBox.ignoreLogs([
9 | "Require cycle:",
10 | ])
11 |
--------------------------------------------------------------------------------
/app/utils/isConferencePassed.ts:
--------------------------------------------------------------------------------
1 | export const isConferencePassed = (now = new Date()) => now > new Date("2023-05-19T23:59:59") // After midnight on May 19th, 2023 conference is passed
2 |
--------------------------------------------------------------------------------
/app/utils/isMounted.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useCallback, useRef } from "react"
2 |
3 | export function useIsMounted() {
4 | const isMounted = useRef(false)
5 |
6 | useEffect(() => {
7 | isMounted.current = true
8 |
9 | return () => {
10 | isMounted.current = false
11 | }
12 | }, [])
13 |
14 | return useCallback(() => isMounted.current, [])
15 | }
16 |
--------------------------------------------------------------------------------
/app/utils/notEmpty.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param value: the value to check within a filter function
4 | * @returns: true if the value is not null or undefined
5 | *
6 | * This is a type guard function. It is used to narrow the type of a value within a filter function.
7 | *
8 | * @example
9 | * ```tsx
10 | * const numbers = [1, 2, 3, null, 4, undefined, 5]
11 | *
12 | * const filteredNumbers = numbers.filter(notEmpty) // type of filteredNumbers is number[]
13 | *
14 | * console.log(filteredNumbers) // [1, 2, 3, 4, 5]
15 | * ```
16 | *
17 | * @example
18 | * without notEmpty, we have the wrong type:
19 | * ```tsx
20 | * const numbers = [1, 2, 3, null, 4, undefined, 5]
21 | *
22 | * const filteredNumbers = numbers.filter(Boolean) // type of filteredNumbers is (number | null | undefined)[]
23 | *
24 | * console.log(filteredNumbers) // [1, 2, 3, 4, 5]
25 | * ```
26 | */
27 | export const notEmpty = (value: TValue | null | undefined): value is TValue =>
28 | value !== null && value !== undefined
29 |
--------------------------------------------------------------------------------
/app/utils/openLinkInBrowser.ts:
--------------------------------------------------------------------------------
1 | import { Linking } from "react-native"
2 |
3 | /**
4 | * Helper to get the social username from a given URL. Mainly Twitter and GitHub.
5 | *
6 | * @param url The URL to extract the username from.
7 | * @returns The username: string
8 | *
9 | * @example
10 | * ```tsx
11 | * getSocialUsername("https://twitter.com/mazenchami") // mazenchami
12 | * ```
13 | */
14 | const getSocialUsername = (url: string) => {
15 | const n = url.lastIndexOf("/")
16 | return url.substring(n + 1)
17 | }
18 |
19 | /**
20 | * Helper to clean a given URL to be used to open the app as a fallback if the app is installed (Android bug).
21 | *
22 | * @param url The URL to clean.
23 | * @returns The cleaned URL: string
24 | *
25 | * @example
26 | * ```tsx
27 | * cleanUrl("https://twitter.com/mazenchami") // twitter://user?screen_name=mazenchami
28 | * ```
29 | */
30 | const cleanUrl = (url: string) => {
31 | if (url.includes("twitter.com")) {
32 | return `twitter://user?screen_name=${getSocialUsername(url)}`
33 | }
34 | return url
35 | }
36 |
37 | /**
38 | * Helper for opening a give URL in an external browser.
39 | */
40 | export function openLinkInBrowser(url: string) {
41 | Linking.canOpenURL(url).then((canOpen) =>
42 | canOpen ? Linking.openURL(url) : Linking.openURL(cleanUrl(url)),
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/app/utils/openMap.tsx:
--------------------------------------------------------------------------------
1 | import { Linking, Platform } from "react-native"
2 |
3 | export const openMap = async (address: string) => {
4 | const destination = encodeURIComponent(address)
5 | const provider = Platform.OS === "ios" ? "apple" : "google"
6 | const link = `https://maps.${provider}.com/?daddr=${destination}`
7 |
8 | try {
9 | // TODO come back here for canOpenURL and properly implement AndroidManifest.xml
10 | // https://github.com/facebook/react-native/pull/31263
11 | // https://developer.android.com/training/package-visibility
12 | // https://github.com/facebook/react-native/issues/32311
13 | Linking.openURL(link)
14 | } catch (error) {}
15 | }
16 |
--------------------------------------------------------------------------------
/app/utils/storage/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./storage"
2 |
--------------------------------------------------------------------------------
/app/utils/storage/storage.test.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from "@react-native-async-storage/async-storage"
2 | import { load, loadString, save, saveString, clear, remove } from "./storage"
3 |
4 | // fixtures
5 | const VALUE_OBJECT = { x: 1 }
6 | const VALUE_STRING = JSON.stringify(VALUE_OBJECT)
7 |
8 | beforeEach(() => (AsyncStorage.getItem as jest.Mock).mockReturnValue(Promise.resolve(VALUE_STRING)))
9 | afterEach(() => jest.clearAllMocks())
10 |
11 | test("load", async () => {
12 | const value = await load("something")
13 | expect(value).toEqual(JSON.parse(VALUE_STRING))
14 | })
15 |
16 | test("loadString", async () => {
17 | const value = await loadString("something")
18 | expect(value).toEqual(VALUE_STRING)
19 | })
20 |
21 | test("save", async () => {
22 | await save("something", VALUE_OBJECT)
23 | expect(AsyncStorage.setItem).toHaveBeenCalledWith("something", VALUE_STRING)
24 | })
25 |
26 | test("saveString", async () => {
27 | await saveString("something", VALUE_STRING)
28 | expect(AsyncStorage.setItem).toHaveBeenCalledWith("something", VALUE_STRING)
29 | })
30 |
31 | test("remove", async () => {
32 | await remove("something")
33 | expect(AsyncStorage.removeItem).toHaveBeenCalledWith("something")
34 | })
35 |
36 | test("clear", async () => {
37 | await clear()
38 | expect(AsyncStorage.clear).toHaveBeenCalledWith()
39 | })
40 |
--------------------------------------------------------------------------------
/app/utils/storage/storage.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from "@react-native-async-storage/async-storage"
2 |
3 | /**
4 | * Loads a string from storage.
5 | *
6 | * @param key The key to fetch.
7 | */
8 | export async function loadString(key: string): Promise {
9 | try {
10 | return await AsyncStorage.getItem(key)
11 | } catch {
12 | // not sure why this would fail... even reading the RN docs I'm unclear
13 | return null
14 | }
15 | }
16 |
17 | /**
18 | * Saves a string to storage.
19 | *
20 | * @param key The key to fetch.
21 | * @param value The value to store.
22 | */
23 | export async function saveString(key: string, value: string): Promise {
24 | try {
25 | await AsyncStorage.setItem(key, value)
26 | return true
27 | } catch {
28 | return false
29 | }
30 | }
31 |
32 | /**
33 | * Loads something from storage and runs it thru JSON.parse.
34 | *
35 | * @param key The key to fetch.
36 | */
37 | export async function load(key: string): Promise {
38 | try {
39 | const almostThere = await AsyncStorage.getItem(key)
40 | return JSON.parse(almostThere ?? "")
41 | } catch {
42 | return null
43 | }
44 | }
45 |
46 | /**
47 | * Saves an object to storage.
48 | *
49 | * @param key The key to fetch.
50 | * @param value The value to store.
51 | */
52 | export async function save(key: string, value: T): Promise {
53 | try {
54 | await AsyncStorage.setItem(key, JSON.stringify(value))
55 | return true
56 | } catch {
57 | return false
58 | }
59 | }
60 |
61 | /**
62 | * Removes something from storage.
63 | *
64 | * @param key The key to kill.
65 | */
66 | export async function remove(key: string): Promise {
67 | try {
68 | await AsyncStorage.removeItem(key)
69 | } catch {}
70 | }
71 |
72 | /**
73 | * Burn it all to the ground.
74 | */
75 | export async function clear(): Promise {
76 | try {
77 | await AsyncStorage.clear()
78 | } catch {}
79 | }
80 |
--------------------------------------------------------------------------------
/app/utils/stringOrPlaceholder.ts:
--------------------------------------------------------------------------------
1 | import { translate, TxKeyPath } from "../i18n"
2 |
3 | /**
4 | * Returns a string if it is not empty, otherwise returns a placeholder
5 | *
6 | * @param string—string—The string to check
7 | * @param placeholder-TxKeyPath-The placeholder to return if the string is empty. Uses our i18n key path format.
8 | * @returns string
9 | *
10 | * @example
11 | * stringOrPlaceholder("Hello World") // "Hello World"
12 | * stringOrPlaceholder("") // "Coming soon"
13 | * stringOrPlaceholder(undefined, "common.ok") // "OK!"
14 | * stringOrPlaceholder("Hello World", "common.ok") // "Hello World"
15 | * stringOrPlaceholder("", "common.ok") // "OK!"
16 | */
17 | export const stringOrPlaceholder = (
18 | string?: string,
19 | placeholder: TxKeyPath = "common.comingSoon",
20 | ) => (string && string.length > 1 ? string : translate(placeholder))
21 |
--------------------------------------------------------------------------------
/assets/branding-banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/branding-banner.jpg
--------------------------------------------------------------------------------
/assets/fonts/GothamRounded-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/fonts/GothamRounded-Bold.otf
--------------------------------------------------------------------------------
/assets/fonts/GothamRounded-Book.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/fonts/GothamRounded-Book.otf
--------------------------------------------------------------------------------
/assets/fonts/GothamRounded-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/fonts/GothamRounded-Medium.otf
--------------------------------------------------------------------------------
/assets/fonts/GothamSSm-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/fonts/GothamSSm-Bold.otf
--------------------------------------------------------------------------------
/assets/fonts/GothamSSm-Book.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/fonts/GothamSSm-Book.otf
--------------------------------------------------------------------------------
/assets/fonts/GothamSSm-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/fonts/GothamSSm-Medium.otf
--------------------------------------------------------------------------------
/assets/google-play-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/google-play-badge.png
--------------------------------------------------------------------------------
/assets/icons/arrowDown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrowDown.png
--------------------------------------------------------------------------------
/assets/icons/arrowDown@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrowDown@2x.png
--------------------------------------------------------------------------------
/assets/icons/arrowDown@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrowDown@3x.png
--------------------------------------------------------------------------------
/assets/icons/arrowUp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrowUp.png
--------------------------------------------------------------------------------
/assets/icons/arrowUp@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrowUp@2x.png
--------------------------------------------------------------------------------
/assets/icons/arrowUp@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrowUp@3x.png
--------------------------------------------------------------------------------
/assets/icons/arrows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrows.png
--------------------------------------------------------------------------------
/assets/icons/arrows@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrows@2x.png
--------------------------------------------------------------------------------
/assets/icons/arrows@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/arrows@3x.png
--------------------------------------------------------------------------------
/assets/icons/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/back.png
--------------------------------------------------------------------------------
/assets/icons/back@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/back@2x.png
--------------------------------------------------------------------------------
/assets/icons/back@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/back@3x.png
--------------------------------------------------------------------------------
/assets/icons/caretLeft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/caretLeft.png
--------------------------------------------------------------------------------
/assets/icons/caretLeft@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/caretLeft@2x.png
--------------------------------------------------------------------------------
/assets/icons/caretLeft@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/caretLeft@3x.png
--------------------------------------------------------------------------------
/assets/icons/caretRight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/caretRight.png
--------------------------------------------------------------------------------
/assets/icons/caretRight@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/caretRight@2x.png
--------------------------------------------------------------------------------
/assets/icons/caretRight@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/caretRight@3x.png
--------------------------------------------------------------------------------
/assets/icons/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/chat.png
--------------------------------------------------------------------------------
/assets/icons/chat@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/chat@2x.png
--------------------------------------------------------------------------------
/assets/icons/chat@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/chat@3x.png
--------------------------------------------------------------------------------
/assets/icons/check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/check.png
--------------------------------------------------------------------------------
/assets/icons/check@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/check@2x.png
--------------------------------------------------------------------------------
/assets/icons/check@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/check@3x.png
--------------------------------------------------------------------------------
/assets/icons/explore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/explore.png
--------------------------------------------------------------------------------
/assets/icons/explore@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/explore@2x.png
--------------------------------------------------------------------------------
/assets/icons/explore@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/explore@3x.png
--------------------------------------------------------------------------------
/assets/icons/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/github.png
--------------------------------------------------------------------------------
/assets/icons/github@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/github@2x.png
--------------------------------------------------------------------------------
/assets/icons/github@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/github@3x.png
--------------------------------------------------------------------------------
/assets/icons/info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/info.png
--------------------------------------------------------------------------------
/assets/icons/info@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/info@2x.png
--------------------------------------------------------------------------------
/assets/icons/info@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/info@3x.png
--------------------------------------------------------------------------------
/assets/icons/ladybug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/ladybug.png
--------------------------------------------------------------------------------
/assets/icons/ladybug@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/ladybug@2x.png
--------------------------------------------------------------------------------
/assets/icons/ladybug@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/ladybug@3x.png
--------------------------------------------------------------------------------
/assets/icons/link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/link.png
--------------------------------------------------------------------------------
/assets/icons/link@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/link@2x.png
--------------------------------------------------------------------------------
/assets/icons/link@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/link@3x.png
--------------------------------------------------------------------------------
/assets/icons/schedule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/schedule.png
--------------------------------------------------------------------------------
/assets/icons/schedule@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/schedule@2x.png
--------------------------------------------------------------------------------
/assets/icons/schedule@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/schedule@3x.png
--------------------------------------------------------------------------------
/assets/icons/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/twitter.png
--------------------------------------------------------------------------------
/assets/icons/twitter@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/twitter@2x.png
--------------------------------------------------------------------------------
/assets/icons/twitter@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/twitter@3x.png
--------------------------------------------------------------------------------
/assets/icons/venue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/venue.png
--------------------------------------------------------------------------------
/assets/icons/venue@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/venue@2x.png
--------------------------------------------------------------------------------
/assets/icons/venue@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/venue@3x.png
--------------------------------------------------------------------------------
/assets/icons/youtube.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/youtube.png
--------------------------------------------------------------------------------
/assets/icons/youtube@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/youtube@2x.png
--------------------------------------------------------------------------------
/assets/icons/youtube@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/icons/youtube@3x.png
--------------------------------------------------------------------------------
/assets/images/app-icon-all.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/app-icon-all.png
--------------------------------------------------------------------------------
/assets/images/app-icon-android-adaptive-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/app-icon-android-adaptive-background.png
--------------------------------------------------------------------------------
/assets/images/app-icon-android-adaptive-foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/app-icon-android-adaptive-foreground.png
--------------------------------------------------------------------------------
/assets/images/app-icon-android-legacy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/app-icon-android-legacy.png
--------------------------------------------------------------------------------
/assets/images/app-icon-ios.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/app-icon-ios.png
--------------------------------------------------------------------------------
/assets/images/app-icon-web-favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/app-icon-web-favicon.png
--------------------------------------------------------------------------------
/assets/images/card-offset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/card-offset.png
--------------------------------------------------------------------------------
/assets/images/card-offset@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/card-offset@2x.png
--------------------------------------------------------------------------------
/assets/images/card-offset@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/card-offset@3x.png
--------------------------------------------------------------------------------
/assets/images/cr-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/cr-logo.png
--------------------------------------------------------------------------------
/assets/images/cr-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/cr-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/cr-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/cr-logo@3x.png
--------------------------------------------------------------------------------
/assets/images/info-conf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-conf.jpg
--------------------------------------------------------------------------------
/assets/images/info-conf@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-conf@2x.jpg
--------------------------------------------------------------------------------
/assets/images/info-conf@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-conf@3x.jpg
--------------------------------------------------------------------------------
/assets/images/info-ir1@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir1@2x.png
--------------------------------------------------------------------------------
/assets/images/info-ir1@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir1@3x.png
--------------------------------------------------------------------------------
/assets/images/info-ir2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir2.png
--------------------------------------------------------------------------------
/assets/images/info-ir2@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir2@2x.png
--------------------------------------------------------------------------------
/assets/images/info-ir2@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir2@3x.png
--------------------------------------------------------------------------------
/assets/images/info-ir3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir3.png
--------------------------------------------------------------------------------
/assets/images/info-ir3@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir3@2x.png
--------------------------------------------------------------------------------
/assets/images/info-ir3@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-ir3@3x.png
--------------------------------------------------------------------------------
/assets/images/info-r1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/info-r1.png
--------------------------------------------------------------------------------
/assets/images/sad-face.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/sad-face.png
--------------------------------------------------------------------------------
/assets/images/sad-face@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/sad-face@2x.png
--------------------------------------------------------------------------------
/assets/images/sad-face@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/sad-face@3x.png
--------------------------------------------------------------------------------
/assets/images/splash-logo-all.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/splash-logo-all.png
--------------------------------------------------------------------------------
/assets/images/splash-logo-android-universal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/splash-logo-android-universal.png
--------------------------------------------------------------------------------
/assets/images/splash-logo-ios-mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/splash-logo-ios-mobile.png
--------------------------------------------------------------------------------
/assets/images/splash-logo-ios-tablet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/splash-logo-ios-tablet.png
--------------------------------------------------------------------------------
/assets/images/splash-logo-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/splash-logo-web.png
--------------------------------------------------------------------------------
/assets/images/talk-curve.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/talk-curve.png
--------------------------------------------------------------------------------
/assets/images/talk-curve@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/talk-curve@2x.png
--------------------------------------------------------------------------------
/assets/images/talk-curve@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/talk-curve@3x.png
--------------------------------------------------------------------------------
/assets/images/talk-shape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/talk-shape.png
--------------------------------------------------------------------------------
/assets/images/talk-shape@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/talk-shape@2x.png
--------------------------------------------------------------------------------
/assets/images/talk-shape@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/talk-shape@3x.png
--------------------------------------------------------------------------------
/assets/images/testdouble-breaks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/testdouble-breaks.png
--------------------------------------------------------------------------------
/assets/images/testdouble-breaks@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/testdouble-breaks@2x.png
--------------------------------------------------------------------------------
/assets/images/testdouble-breaks@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/testdouble-breaks@3x.png
--------------------------------------------------------------------------------
/assets/images/welcome-shapes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/welcome-shapes.png
--------------------------------------------------------------------------------
/assets/images/welcome-shapes@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/welcome-shapes@2x.png
--------------------------------------------------------------------------------
/assets/images/welcome-shapes@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/welcome-shapes@3x.png
--------------------------------------------------------------------------------
/assets/images/workshop-curve.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/workshop-curve.png
--------------------------------------------------------------------------------
/assets/images/workshop-curve@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/workshop-curve@2x.png
--------------------------------------------------------------------------------
/assets/images/workshop-curve@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/workshop-curve@3x.png
--------------------------------------------------------------------------------
/assets/images/workshop-shape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/workshop-shape.png
--------------------------------------------------------------------------------
/assets/images/workshop-shape@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/workshop-shape@2x.png
--------------------------------------------------------------------------------
/assets/images/workshop-shape@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/infinitered/ChainReactApp2023/cb1c2b9e6225dbfbe4077f6ddfbca3d5c9629237/assets/images/workshop-shape@3x.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const plugins = [
2 | "react-native-reanimated/plugin", // NOTE: this must be last in the plugins
3 | ]
4 |
5 | const expoConfig = {
6 | presets: ["babel-preset-expo"],
7 | env: {
8 | production: {},
9 | },
10 | plugins,
11 | }
12 |
13 | module.exports = expoConfig
14 |
--------------------------------------------------------------------------------
/bin/postInstall:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /**
3 | * Do all things that need to be done after installing packages (with yarn, npm, pnpm).
4 | *
5 | * Yes, it slows down package installation a little, but it's nice to not
6 | * have to remember these extra steps.
7 | */
8 |
9 | // Patch any packages that need patching
10 | run("npx patch-package")
11 |
12 | // Kill the metro bundler if it's running.
13 | if (["darwin", "linux"].includes(process.platform)) {
14 | run('pkill -f "cli.js start" || set exit 0')
15 | }
16 |
17 | // Run baby run
18 | function run(command) {
19 | console.log(`./bin/postInstall script running: ${command}`)
20 |
21 | try {
22 | require("child_process").execSync(command, { stdio: "inherit" })
23 | } catch (error) {
24 | console.error(`./bin/postInstall failed on command:\n ${command}`)
25 | process.exit(error.status)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 3.1.1"
4 | },
5 | "build": {
6 | "development": {
7 | "extends": "production",
8 | "channel": "development-simulator",
9 | "developmentClient": true,
10 | "distribution": "internal",
11 | "android": {
12 | "gradleCommand": ":app:assembleDebug"
13 | },
14 | "ios": {
15 | "buildConfiguration": "Debug",
16 | "simulator": true
17 | }
18 | },
19 | "development:device": {
20 | "extends": "development",
21 | "channel": "development-device",
22 | "developmentClient": true,
23 | "distribution": "internal",
24 | "ios": {
25 | "buildConfiguration": "Debug",
26 | "simulator": false
27 | }
28 | },
29 | "preview": {
30 | "extends": "production",
31 | "channel": "preview",
32 | "distribution": "internal",
33 | "ios": { "simulator": true },
34 | "android": { "buildType": "apk" },
35 | "env": {}
36 | },
37 | "preview:device": {
38 | "extends": "preview",
39 | "channel": "preview-device",
40 | "ios": { "simulator": false }
41 | },
42 | "production": {
43 | "channel": "production",
44 | "env": {},
45 | "ios": {
46 | "resourceClass": "m-medium"
47 | },
48 | "android": {
49 | "buildType": "apk"
50 | }
51 | }
52 | },
53 | "submit": {
54 | "production": {
55 | "ios": {
56 | "ascAppId": "1239112816",
57 | "appleTeamId": "L7YNDPLSEB"
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "1051103944258",
4 | "project_id": "chainreactapp2023",
5 | "storage_bucket": "chainreactapp2023.appspot.com"
6 | },
7 | "client": [
8 | {
9 | "client_info": {
10 | "mobilesdk_app_id": "1:1051103944258:android:28c3e3c2aac23be9a157db",
11 | "android_client_info": {
12 | "package_name": "com.chainreactapp"
13 | }
14 | },
15 | "oauth_client": [
16 | {
17 | "client_id": "1051103944258-cm6hlao4iin0h0169gonceel0nccrfrk.apps.googleusercontent.com",
18 | "client_type": 3
19 | }
20 | ],
21 | "api_key": [
22 | {
23 | "current_key": "AIzaSyAOtLvFMcMP2tPrO6Y_9KV9XqPkq2ACLG0"
24 | }
25 | ],
26 | "services": {
27 | "appinvite_service": {
28 | "other_platform_oauth_client": [
29 | {
30 | "client_id": "1051103944258-cm6hlao4iin0h0169gonceel0nccrfrk.apps.googleusercontent.com",
31 | "client_type": 3
32 | },
33 | {
34 | "client_id": "1051103944258-1ser3n77nmsf0o1vml734oo31i3v8qft.apps.googleusercontent.com",
35 | "client_type": 2,
36 | "ios_info": {
37 | "bundle_id": "infinitered.stage.ChainReactConf"
38 | }
39 | }
40 | ]
41 | }
42 | }
43 | }
44 | ],
45 | "configuration_version": "1"
46 | }
47 |
--------------------------------------------------------------------------------
/ignite/templates/component/NAME.tsx.ejs:
--------------------------------------------------------------------------------
1 | ---
2 | patch:
3 | path: "app/components/index.ts"
4 | append: "export * from \"./<%= props.pascalCaseName %>\"\n"
5 | skip: <%= props.skipIndexFile %>
6 | ---
7 | import * as React from "react"
8 | import { StyleProp, TextStyle, View, ViewStyle } from "react-native"
9 | import { colors, typography } from "../theme"
10 | import { Text } from "./Text"
11 |
12 | export interface <%= props.pascalCaseName %>Props {
13 | /**
14 | * An optional style override useful for padding & margin.
15 | */
16 | style?: StyleProp
17 | }
18 |
19 | /**
20 | * Describe your component here
21 | */
22 | export function <%= props.pascalCaseName %>(props: <%= props.pascalCaseName %>Props) {
23 | const { style } = props
24 | const $styles = Object.assign({}, $container, style)
25 |
26 | return (
27 |
28 | Hello
29 |
30 | )
31 | }
32 |
33 | const $container: ViewStyle = {
34 | justifyContent: "center",
35 | }
36 |
37 | const $text: TextStyle = {
38 | fontFamily: typography.primary.book,
39 | fontSize: 14,
40 | color: colors.palette.primary500,
41 | }
42 |
--------------------------------------------------------------------------------
/ignite/templates/navigator/NAMENavigator.tsx.ejs:
--------------------------------------------------------------------------------
1 | ---
2 | destinationDir: app/navigators
3 | patch:
4 | path: "app/navigators/index.ts"
5 | append: "export * from \"./<%= props.pascalCaseName %>Navigator\"\n"
6 | skip: <%= props.skipIndexFile %>
7 | ---
8 | import React from "react"
9 | import { createStackNavigator } from "@react-navigation/stack"
10 | import {
11 | WelcomeScreen
12 | } from "../screens"
13 |
14 | export type <%= props.pascalCaseName %>NavigatorParamList = {
15 | Demo: undefined
16 | }
17 |
18 | const Stack = createStackNavigator<<%= props.pascalCaseName %>NavigatorParamList>()
19 | export const <%= props.pascalCaseName %>Navigator = () => {
20 | return (
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/ignite/templates/screen/NAMEScreen.tsx.ejs:
--------------------------------------------------------------------------------
1 | ---
2 | patch:
3 | path: "app/screens/index.ts"
4 | append: "export * from \"./<%= props.pascalCaseName %>Screen\"\n"
5 | skip: <%= props.skipIndexFile %>
6 | ---
7 | import React, { FC } from "react"
8 | import { ViewStyle } from "react-native"
9 | import { StackScreenProps } from "@react-navigation/stack"
10 | import { AppStackParamList } from "../navigators"
11 | import { Screen, Text } from "../components"
12 | // import { useAppNavigation } from "../hooks"
13 |
14 | // STOP! READ ME FIRST!
15 | // To fix the TS error below, you'll need to add the following things in your navigation config:
16 | // - Add `<%= props.pascalCaseName %>: undefined` to AppStackParamList
17 | // - Import your screen, and add it to the stack:
18 | // `Screen} />`
19 | // Hint: Look for the 🔥!
20 |
21 | // REMOVE ME! ⬇️ This TS ignore will not be necessary after you've added the correct navigator param type
22 | // @ts-ignore
23 | export const <%= props.pascalCaseName %>Screen: FC">> = () => {
24 | // Pull in navigation via hook
25 | // const navigation = useAppNavigation()
26 | return (
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | const $root: ViewStyle = {
34 | flex: 1,
35 | }
36 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { defaults: tsjPreset } = require("ts-jest/presets")
2 |
3 | module.exports = {
4 | ...tsjPreset,
5 | preset: "jest-expo",
6 | globals: {
7 | "ts-jest": {
8 | babelConfig: true,
9 | },
10 | },
11 | transformIgnorePatterns: [
12 | "/node_modules/(react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|@unimodules/.*|native-base|react-native-code-push)",
13 | ],
14 | testPathIgnorePatterns: ["/node_modules/", "/detox", "@react-native"],
15 | testEnvironment: "jsdom",
16 | setupFiles: ["/test/setup.ts"],
17 | }
18 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require('expo/metro-config')
2 |
3 | /**
4 | * Expo metro config
5 | * Learn more https://docs.expo.io/guides/customizing-metro
6 |
7 | * For one idea on how to support symlinks in Expo, see:
8 | * https://github.com/infinitered/ignite/issues/1904#issuecomment-1054535068
9 | */
10 | const metroConfig = getDefaultConfig(__dirname)
11 |
12 | module.exports = metroConfig
13 |
--------------------------------------------------------------------------------
/react-native.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | assets: [
3 | // TODO: link documentation about fonts to this section
4 | // If you need to add non-google fonts (those not available through the `@expo-google-fonts/**`
5 | // packages, you can add them to the referenced path below and uncomment this line.
6 | // "./assets/fonts/"
7 | ],
8 | }
9 |
--------------------------------------------------------------------------------
/test/i18n.test.ts:
--------------------------------------------------------------------------------
1 | import en from "../app/i18n/en"
2 | import { exec } from "child_process"
3 |
4 | // Use this array for keys that for whatever reason aren't greppable so they
5 | // don't hold your test suite hostage by always failing.
6 | const EXCEPTIONS: Array = [
7 | // "welcomeScreen.readyForLaunch",
8 | ]
9 |
10 | function iterate(obj: Record, stack: string, array: Array) {
11 | for (const [property, value] of Object.entries(obj)) {
12 | if (Object.prototype.hasOwnProperty.call(obj, property)) {
13 | if (typeof value === "object") {
14 | iterate(value, `${stack}.${property}`, array)
15 | } else {
16 | array.push(`${stack.slice(1)}.${property}`)
17 | }
18 | }
19 | }
20 |
21 | return array
22 | }
23 |
24 | /**
25 | * This tests your codebase for missing i18n strings so you can avoid error strings at render time
26 | *
27 | * It was taken from https://gist.github.com/Michaelvilleneuve/8808ba2775536665d95b7577c9d8d5a1
28 | * and modified slightly to account for our Ignite higher order components,
29 | * which take 'tx' and 'fooTx' props.
30 | * The grep command is nasty looking, but it's essentially searching the codebase for a few different things:
31 | *
32 | * tx="*"
33 | * Tx=""
34 | * tx={""}
35 | * Tx={""}
36 | * translate(""
37 | *
38 | * and then grabs the i18n key between the double quotes
39 | *
40 | * This approach isn't 100% perfect. If you are storing your key string in a variable because you
41 | * are setting it conditionally, then it won't be picked up.
42 | *
43 | */
44 |
45 | describe("i18n", () => {
46 | test("There are no missing keys", (done) => {
47 | // Actual command output:
48 | // grep "[T\|t]x=[{]\?\"\S*\"[}]\?\|translate(\"\S*\"" -ohr './app' | grep -o "\".*\""
49 | const command = `grep "[T\\|t]x=[{]\\?\\"\\S*\\"[}]\\?\\|translate(\\"\\S*\\"" -ohr './app' | grep -o "\\".*\\""`
50 | exec(command, (_, stdout) => {
51 | const allTranslationsDefined = iterate(en, "", [])
52 | const allTranslationsUsed = stdout.replace(/"/g, "").split("\n")
53 | allTranslationsUsed.splice(-1, 1)
54 |
55 | for (let i = 0; i < allTranslationsUsed.length; i += 1) {
56 | if (!EXCEPTIONS.includes(allTranslationsUsed[i])) {
57 | // You can add keys to EXCEPTIONS (above) if you don't want them included in the test
58 | expect(allTranslationsDefined).toContainEqual(allTranslationsUsed[i])
59 | }
60 | }
61 | done()
62 | })
63 | }, 240000)
64 | })
65 |
--------------------------------------------------------------------------------
/test/mockFile.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | height: 100,
3 | width: 100,
4 | scale: 2.0,
5 | uri: "https://placekitten.com/200/200",
6 | }
7 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | // we always make sure 'react-native' gets included first
2 | import * as ReactNative from "react-native"
3 | import mockAsyncStorage from "@react-native-async-storage/async-storage/jest/async-storage-mock"
4 | import mockFile from "./mockFile"
5 |
6 | // libraries to mock
7 | jest.doMock("react-native", () => {
8 | // Extend ReactNative
9 | return Object.setPrototypeOf(
10 | {
11 | Image: {
12 | ...ReactNative.Image,
13 | resolveAssetSource: jest.fn((_source) => mockFile), // eslint-disable-line @typescript-eslint/no-unused-vars
14 | getSize: jest.fn(
15 | (
16 | uri: string, // eslint-disable-line @typescript-eslint/no-unused-vars
17 | success: (width: number, height: number) => void,
18 | failure?: (_error: any) => void, // eslint-disable-line @typescript-eslint/no-unused-vars
19 | ) => success(100, 100),
20 | ),
21 | },
22 | },
23 | ReactNative,
24 | )
25 | })
26 |
27 | jest.mock("@react-native-async-storage/async-storage", () => mockAsyncStorage)
28 |
29 | jest.mock("i18n-js", () => ({
30 | currentLocale: () => "en",
31 | t: (key: string, params: Record) => {
32 | return `${key} ${JSON.stringify(params)}`
33 | },
34 | }))
35 |
36 | jest.useFakeTimers()
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": false,
4 | "allowSyntheticDefaultImports": true,
5 | "experimentalDecorators": true,
6 | "jsx": "react-native",
7 | "module": "es2015",
8 | "moduleResolution": "node",
9 | "noImplicitAny": true,
10 | "noImplicitReturns": true,
11 | "noImplicitThis": true,
12 | "noUnusedLocals": false,
13 | "sourceMap": true,
14 | "target": "esnext",
15 | "lib": ["esnext", "dom"],
16 | "skipLibCheck": true,
17 | "resolveJsonModule": true,
18 | "strict": true
19 | },
20 | "exclude": ["node_modules"],
21 | "include": ["index.js", "App.js", "app", "test"],
22 | "extends": "expo/tsconfig.base"
23 | }
24 |
--------------------------------------------------------------------------------