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