diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index b8546a62183113..7d17faabd97c7e 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -52,13 +52,15 @@ jobs: NODE_ENV: production run: yarn lint --max-warnings 0 - name: ๐Ÿ’ฌ Lint Docs website content - uses: errata-ai/vale-action@reviewdog + uses: errata-ai/vale-action@v2.1.1 with: version: 3.12.0 reporter: github-pr-check files: 'docs/pages' vale_flags: '--config=./docs/.vale.ini' fail_on_error: true + # Override bundled reviewdog (0.17.0) to avoid 300-file diff limit + reviewdog_url: https://github.com/reviewdog/reviewdog/releases/download/v0.21.0/reviewdog_0.21.0_Linux_x86_64.tar.gz - name: ๐Ÿ—๏ธ Build Docs website run: yarn export-preview timeout-minutes: 20 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 16f0bbeeceb161..82fee7ce0c85ac 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -62,13 +62,15 @@ jobs: NODE_ENV: production run: yarn lint --max-warnings 0 - name: ๐Ÿ’ฌ Lint Docs website content - uses: errata-ai/vale-action@reviewdog + uses: errata-ai/vale-action@v2.1.1 with: version: 3.12.0 reporter: github-pr-check files: 'docs/pages' vale_flags: '--config=./docs/.vale.ini' fail_on_error: true + # Override bundled reviewdog (0.17.0) to avoid 300-file diff limit + reviewdog_url: https://github.com/reviewdog/reviewdog/releases/download/v0.21.0/reviewdog_0.21.0_Linux_x86_64.tar.gz - name: ๐Ÿ—๏ธ Build Docs website for deploy working-directory: docs run: yarn export diff --git a/.github/workflows/home.yml b/.github/workflows/home.yml deleted file mode 100644 index 707ac0dd48b8c6..00000000000000 --- a/.github/workflows/home.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Home app - -on: - workflow_dispatch: {} - pull_request: - paths: - - .github/workflows/home.yml - - apps/expo-go/** - - react-native-lab/** - - yarn.lock - push: - branches: [main, 'sdk-*'] - paths: - - .github/workflows/home.yml - - apps/expo-go/** - - react-native-lab/** - - yarn.lock - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-24.04 - steps: - - name: ๐Ÿ‘€ Checkout - uses: actions/checkout@v4 - with: - submodules: true - - - name: โฌข Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: โ™ป๏ธ Restore caches - uses: ./.github/actions/expo-caches - id: expo-caches - with: - yarn-workspace: 'true' - - - name: ๐Ÿงถ Yarn install - if: steps.expo-caches.outputs.yarn-workspace-hit != 'true' - run: yarn install --frozen-lockfile - - - name: ๐Ÿงถ Yarn install (react-native-lab) - run: yarn install --frozen-lockfile - working-directory: react-native-lab/react-native - - - name: ๐Ÿ›  Compile Home sources - run: yarn tsc - working-directory: apps/expo-go - - - name: ๐Ÿงช Run Home tests - run: yarn jest --maxWorkers 1 - working-directory: apps/expo-go - - - name: ๐Ÿšจ Lint Home app - run: yarn lint --max-warnings 0 - working-directory: apps/expo-go - - - name: ๐Ÿ‘ท Build Home app - run: yarn expo export - working-directory: apps/expo-go - - - name: ๐Ÿ”” Notify on Slack - uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) - with: - webhook: ${{ secrets.slack_webhook_api }} - author_name: Home app diff --git a/.github/workflows/ios-static-frameworks.yml b/.github/workflows/ios-static-frameworks.yml index 8334a3ec8d92e0..d10856e3572f97 100644 --- a/.github/workflows/ios-static-frameworks.yml +++ b/.github/workflows/ios-static-frameworks.yml @@ -42,7 +42,8 @@ jobs: - name: ๐Ÿ Build iOS Project working-directory: ./apps/bare-expo run: | - jq '.["ios.useFrameworks"] = "static"' ios/Podfile.properties.json > temp.json && mv temp.json ios/Podfile.properties.json + set -o pipefail + jq '.["ios.useFrameworks"] = "static" | .["ios.buildReactNativeFromSource"] = "true"' ios/Podfile.properties.json > temp.json && mv temp.json ios/Podfile.properties.json pod install --project-directory=ios xcodebuild -workspace ios/BareExpo.xcworkspace -scheme BareExpo -configuration Release -sdk iphonesimulator -derivedDataPath "ios/build" | xcpretty - name: ๐Ÿ”” Notify on Slack diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 4faead9ad86b19..6486bc76dbcd35 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -176,6 +176,11 @@ jobs: env: MAESTRO_CLI_NO_ANALYTICS: 1 timeout-minutes: 40 + - name: ๐Ÿ“ฆ Prepare testing artifacts + if: always() + run: | + # Rename .maestro to maestroArtifacts so it's not a hidden folder when downloaded locally + mv ~/.maestro ~/maestroArtifacts - name: ๐Ÿ“ธ Store testing artifacts if: always() uses: actions/upload-artifact@v4 @@ -183,10 +188,9 @@ jobs: with: name: bare-expo-artifacts-ios path: | - ~/.maestro/tests/**/* + ~/maestroArtifacts/tests/**/* ~/Library/Logs/maestro/**/* overwrite: true - include-hidden-files: true # .maestro is skipped otherwise - name: ๐Ÿ”— Artifacts download URL if: always() run: | @@ -342,6 +346,11 @@ jobs: # Run the actual tests ./scripts/start-android-e2e-test.ts --test working-directory: ./apps/bare-expo + - name: ๐Ÿ“ฆ Prepare testing artifacts + if: always() + run: | + # Rename .maestro to maestroArtifacts so it's not a hidden folder when downloaded locally + mv ~/.maestro ~/maestroArtifacts - name: ๐Ÿ“ธ Store testing artifacts if: always() id: upload-artifacts @@ -349,7 +358,7 @@ jobs: with: name: bare-expo-artifacts-android path: | - ~/.maestro/tests/**/* + ~/maestroArtifacts/tests/**/* overwrite: true - name: ๐Ÿ”— Artifacts download URL if: always() diff --git a/apps/bare-expo/MainNavigator.tsx b/apps/bare-expo/MainNavigator.tsx index bb55c20cd5a2b9..6ddee972424da1 100644 --- a/apps/bare-expo/MainNavigator.tsx +++ b/apps/bare-expo/MainNavigator.tsx @@ -8,6 +8,7 @@ import { StatusBar } from 'expo-status-bar'; import React from 'react'; import { Platform } from 'react-native'; import { TestStackNavigator } from 'test-suite/TestStackNavigator'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; type NavigationRouteConfigMap = React.ComponentType; @@ -154,21 +155,23 @@ export default function MainNavigator() { return null; } return ( - { - AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state)).catch(console.error); - }}> - - {Redirect && } - {Search && } - - - - + + { + AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state)).catch(console.error); + }}> + + {Redirect && } + {Search && } + + + + + ); } diff --git a/apps/bare-expo/android/app/src/main/AndroidManifest.xml b/apps/bare-expo/android/app/src/main/AndroidManifest.xml index 65f782420af2a8..faff9e274516f2 100644 --- a/apps/bare-expo/android/app/src/main/AndroidManifest.xml +++ b/apps/bare-expo/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + @@ -30,6 +31,7 @@ + @@ -42,7 +44,7 @@ - + diff --git a/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainApplication.kt b/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainApplication.kt index 803e57995ecded..98f18b919e4c36 100644 --- a/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainApplication.kt +++ b/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainApplication.kt @@ -20,7 +20,7 @@ class MainApplication : Application(), ReactApplication { ExpoReactHostFactory.getDefaultReactHost( context = applicationContext, packageList = - PackageList(this).packages.apply { + expo.modules.benchmark.withBenchmarkingPackages(PackageList(this).packages).apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) } diff --git a/apps/bare-expo/android/app/src/main/res/drawable-hdpi/location_foreground_service_icon.png b/apps/bare-expo/android/app/src/main/res/drawable-hdpi/location_foreground_service_icon.png new file mode 100644 index 00000000000000..224b8c624052c9 Binary files /dev/null and b/apps/bare-expo/android/app/src/main/res/drawable-hdpi/location_foreground_service_icon.png differ diff --git a/apps/bare-expo/android/app/src/main/res/drawable-mdpi/location_foreground_service_icon.png b/apps/bare-expo/android/app/src/main/res/drawable-mdpi/location_foreground_service_icon.png new file mode 100644 index 00000000000000..1d99dc43bd0ac0 Binary files /dev/null and b/apps/bare-expo/android/app/src/main/res/drawable-mdpi/location_foreground_service_icon.png differ diff --git a/apps/bare-expo/android/app/src/main/res/drawable-xhdpi/location_foreground_service_icon.png b/apps/bare-expo/android/app/src/main/res/drawable-xhdpi/location_foreground_service_icon.png new file mode 100644 index 00000000000000..8a5074ebdac2e9 Binary files /dev/null and b/apps/bare-expo/android/app/src/main/res/drawable-xhdpi/location_foreground_service_icon.png differ diff --git a/apps/bare-expo/android/app/src/main/res/drawable-xxhdpi/location_foreground_service_icon.png b/apps/bare-expo/android/app/src/main/res/drawable-xxhdpi/location_foreground_service_icon.png new file mode 100644 index 00000000000000..1da7df4644936a Binary files /dev/null and b/apps/bare-expo/android/app/src/main/res/drawable-xxhdpi/location_foreground_service_icon.png differ diff --git a/apps/bare-expo/android/app/src/main/res/drawable-xxxhdpi/location_foreground_service_icon.png b/apps/bare-expo/android/app/src/main/res/drawable-xxxhdpi/location_foreground_service_icon.png new file mode 100644 index 00000000000000..1464e949c1664c Binary files /dev/null and b/apps/bare-expo/android/app/src/main/res/drawable-xxxhdpi/location_foreground_service_icon.png differ diff --git a/apps/bare-expo/app.json b/apps/bare-expo/app.json index d967293485ad79..a64d7d93d0bbea 100644 --- a/apps/bare-expo/app.json +++ b/apps/bare-expo/app.json @@ -65,7 +65,8 @@ "expo-location", { "isAndroidBackgroundLocationEnabled": true, - "isIosBackgroundLocationEnabled": true + "isIosBackgroundLocationEnabled": true, + "androidForegroundServiceIcon": "./assets/location_service_icon.png" } ], [ diff --git a/apps/bare-expo/assets/location_service_icon.png b/apps/bare-expo/assets/location_service_icon.png new file mode 100644 index 00000000000000..da5b349c3d4585 Binary files /dev/null and b/apps/bare-expo/assets/location_service_icon.png differ diff --git a/apps/bare-expo/e2e/README.md b/apps/bare-expo/e2e/README.md index c438cabf55f4a4..b494ecdc6c5c9b 100644 --- a/apps/bare-expo/e2e/README.md +++ b/apps/bare-expo/e2e/README.md @@ -6,6 +6,7 @@ - `brew install oxipng` for image compression - run `yarn install` in `bare-expo/e2e/image-comparison` - (optional, recommended) Alignment with devices which are used in CI ([iOS](https://github.com/expo/expo/blob/051a306ce7c5b875f7398450e5aeec2e52e313ae/apps/bare-expo/scripts/start-ios-e2e-test.ts#L18), [Android](https://github.com/expo/expo/blob/051a306ce7c5b875f7398450e5aeec2e52e313ae/.github/actions/use-android-emulator/action.yml#L48)). This is necessary for assertions on what is visible on the screen and (especially) for view shots to match. +- (optional, recommended) Build the iOS screen inspector - run `./scripts/build.sh` in `bare-expo/e2e/image-comparison/inspector`. You can learn more about the inspector in its [README.md](./image-comparison/inspector/README.md) - use the following commands to generate the Android emulator: ```bash diff --git a/apps/bare-expo/e2e/_nested-flows/viewshot-comparison.yaml b/apps/bare-expo/e2e/_nested-flows/viewshot-comparison.yaml index 912539050fc57e..7ff080f8b1c2a6 100644 --- a/apps/bare-expo/e2e/_nested-flows/viewshot-comparison.yaml +++ b/apps/bare-expo/e2e/_nested-flows/viewshot-comparison.yaml @@ -14,6 +14,11 @@ appId: dev.expo.Payments --- +- extendedWaitUntil: + visible: + id: ${testID} + timeout: 10000 + - runScript: file: ./compare-images-http.js env: diff --git a/apps/bare-expo/e2e/expo-video/fullscreen-test.yaml b/apps/bare-expo/e2e/expo-video/fullscreen-test.yaml new file mode 100644 index 00000000000000..d2648c044db9eb --- /dev/null +++ b/apps/bare-expo/e2e/expo-video/fullscreen-test.yaml @@ -0,0 +1,32 @@ +# Tests whether the fullscreen opens, and if appropriate events were sent +appId: dev.expo.Payments +jsEngine: graaljs +--- +- openLink: bareexpo://components/video/fullscreen + +- tapOn: "Enter Fullscreen" +- runFlow: + when: + platform: Android + commands: + # Dismiss the android system fullscreen instruction popup + - tapOn: + text: "Got it" + optional: true + - assertVisible: + id: "dev.expo.payments:id/exo_content_frame" +- runFlow: + when: + platform: iOS + commands: + - assertVisible: "Video" + +- assertNotVisible: "Enter Fullscreen" +- tapOn: + point: '${maestro.platform == "android" ? "94%,97%" : "11%,10%"}' + delay: 400 + repeat: 2 + +- assertVisible: "Enter Fullscreen" +- assertVisible: "0 = onFullscreenEnter" +- assertVisible: "1 = onFullscreenExit" diff --git a/apps/bare-expo/e2e/expo-video/picture-in-picture-test.android.yaml b/apps/bare-expo/e2e/expo-video/picture-in-picture-test.android.yaml new file mode 100644 index 00000000000000..dc09eacae521be --- /dev/null +++ b/apps/bare-expo/e2e/expo-video/picture-in-picture-test.android.yaml @@ -0,0 +1,28 @@ +# Tests whether the fullscreen opens, and if appropriate events were sent +appId: dev.expo.Payments +jsEngine: graaljs +--- +- openLink: bareexpo://components/video/pip + +- tapOn: + text: 'e2e pause' +- tapOn: + text: 'Enter Picture In Picture' +# We cannot do a view-shot check while in pip, because the VideoView with testID is removed when entering. +# We can be pretty sure we've successfully entered PiP when it's gone though +- assertNotVisible: + id: 'pip-view' + +# Important! If you are running this locally and the test fails, it may mean that at some point you have manually shifted the pip window position or size. +# The PiP window position/size is remembered across app and device restarts. So you may have to clean the emulator data to restore the original pip window position. +- tapOn: + point: 68%,68% + repeat: 2 + delay: 700 +# Make sure the view hierarchy was restored +- runFlow: + file: ../_nested-flows/viewshot-comparison.yaml + env: + screenshotOutputPath: 'expo-video/screenshots/pip-1' + testID: 'pip-view' + mode: 'keep-originals' diff --git a/apps/bare-expo/e2e/expo-video/playback-test.yaml b/apps/bare-expo/e2e/expo-video/playback-test.yaml new file mode 100644 index 00000000000000..871755cba9eebf --- /dev/null +++ b/apps/bare-expo/e2e/expo-video/playback-test.yaml @@ -0,0 +1,42 @@ +appId: dev.expo.Payments +jsEngine: graaljs +--- +# when devving, start from different screen so state is reset +#- openLink: bareexpo://components +- openLink: bareexpo://components/video/events + +- assertVisible: 'source = Big Buck Bunny' +- assertVisible: 'isPlaying = false' +- assertVisible: 'isAtStart = true' +- assertVisible: 'duration = 596' +- assertVisible: 'currentTime = 0' +- assertVisible: 'mimeType = video/avc' +- assertVisible: 'isSupported = true' +- assertVisible: 'bitratePositive = true' +- assertVisible: 'volume = 1' +- assertVisible: 'status = readyToPlay' +- assertVisible: 'playbackRate = 1' +- assertVisible: 'error = false' + +- tapOn: Play + +- assertVisible: 'isPlaying = true' +- assertVisible: 'isAtStart = false' + +- tapOn: Pause +- tapOn: Seek to 30s +- assertVisible: 'currentTime = 30' +- runFlow: + file: ../_nested-flows/viewshot-comparison.yaml + env: + screenshotOutputPath: 'expo-video/screenshots/playback-test-1' + testID: 'video-view' + mode: 'keep-originals' + +- tapOn: Trigger an Error + +- assertVisible: 'source = No title' +- assertVisible: 'isPlaying = false' + +- assertVisible: 'status = error' +- assertVisible: 'error = true' diff --git a/apps/bare-expo/e2e/expo-video/player-output-test.yaml b/apps/bare-expo/e2e/expo-video/player-output-test.yaml new file mode 100644 index 00000000000000..61cd791ff9698d --- /dev/null +++ b/apps/bare-expo/e2e/expo-video/player-output-test.yaml @@ -0,0 +1,45 @@ +appId: dev.expo.Payments +jsEngine: graaljs +--- +- openLink: bareexpo://components/video/player_prop + +- tapOn: "Prepare for e2e" +- tapOn: "Move player 1 to the next video view" +- runFlow: + file: ../_nested-flows/viewshot-comparison.yaml + env: + screenshotOutputPath: 'expo-video/screenshots/player-output-1' + testID: 'view-player-output' + mode: 'keep-originals' +- tapOn: + text: "Move player 2 to the next video view" + repeat: 2 +- tapOn: "Move player 1 to the next video view" +- runFlow: + file: ../_nested-flows/viewshot-comparison.yaml + env: + screenshotOutputPath: 'expo-video/screenshots/player-output-2' + testID: 'view-player-output' + mode: 'keep-originals' +- tapOn: "Move player 1 to first video view" +- tapOn: "Move player 2 to first video view" +- runFlow: + file: ../_nested-flows/viewshot-comparison.yaml + env: + screenshotOutputPath: 'expo-video/screenshots/player-output-3' + testID: 'view-player-output' + mode: 'keep-originals' +- tapOn: + id: switch-1 +- tapOn: + text: "Move player 2 to the next video view" + repeat: 3 +- tapOn: + text: "Move player 1 to the next video view" + repeat: 2 +- runFlow: + file: ../_nested-flows/viewshot-comparison.yaml + env: + screenshotOutputPath: 'expo-video/screenshots/player-output-4' + testID: 'view-player-output' + mode: 'keep-originals' diff --git a/apps/bare-expo/e2e/expo-video/screenshots/pip-1/pip-view.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/pip-1/pip-view.base.android.png new file mode 100644 index 00000000000000..84f17163f5e00e Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/pip-1/pip-view.base.android.png differ diff --git a/apps/bare-expo/e2e/expo-video/video-view.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.android.png similarity index 100% rename from apps/bare-expo/e2e/expo-video/video-view.base.android.png rename to apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.android.png diff --git a/apps/bare-expo/e2e/expo-video/video-view.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.ios.png similarity index 100% rename from apps/bare-expo/e2e/expo-video/video-view.base.ios.png rename to apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.ios.png diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.android.png new file mode 100644 index 00000000000000..c4f6e170788d68 Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.android.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.ios.png new file mode 100644 index 00000000000000..ac73f44415ce4c Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.ios.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.android.png new file mode 100644 index 00000000000000..6ef080f50863f6 Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.android.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.ios.png new file mode 100644 index 00000000000000..1fc2985feb49f6 Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.ios.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-3/view-player-output.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-3/view-player-output.base.android.png new file mode 100644 index 00000000000000..1c08c419ce4165 Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-3/view-player-output.base.android.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-3/view-player-output.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-3/view-player-output.base.ios.png new file mode 100644 index 00000000000000..d59fa926238783 Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-3/view-player-output.base.ios.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.android.png new file mode 100644 index 00000000000000..9f0537517f528c Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.android.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.ios.png new file mode 100644 index 00000000000000..f6f09c73f1dd60 Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.ios.png differ diff --git a/apps/bare-expo/e2e/expo-video/screenshots/surface-type-1/surface-type-test.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/surface-type-1/surface-type-test.base.android.png new file mode 100644 index 00000000000000..b3010c6ac5dbe0 Binary files /dev/null and b/apps/bare-expo/e2e/expo-video/screenshots/surface-type-1/surface-type-test.base.android.png differ diff --git a/apps/bare-expo/e2e/expo-video/surface-type-test.android.yaml b/apps/bare-expo/e2e/expo-video/surface-type-test.android.yaml new file mode 100644 index 00000000000000..f8d221c77fd07b --- /dev/null +++ b/apps/bare-expo/e2e/expo-video/surface-type-test.android.yaml @@ -0,0 +1,12 @@ +appId: dev.expo.Payments +jsEngine: graaljs +--- +- openLink: bareexpo://components/video/video-view-surface-type + +- tapOn: "e2e setup" +- runFlow: + file: ../_nested-flows/viewshot-comparison.yaml + env: + screenshotOutputPath: 'expo-video/screenshots/surface-type-1' + testID: 'surface-type-test' + mode: 'keep-originals' diff --git a/apps/bare-expo/e2e/expo-video/test.yaml b/apps/bare-expo/e2e/expo-video/test.yaml deleted file mode 100644 index eb55bd0d7f7bf9..00000000000000 --- a/apps/bare-expo/e2e/expo-video/test.yaml +++ /dev/null @@ -1,45 +0,0 @@ -appId: dev.expo.Payments -jsEngine: graaljs ---- -# when devving, start from different screen so state is reset -#- openLink: bareexpo://components -- openLink: bareexpo://components/video/events -- tapOn: - text: 'Open' - optional: true - -- assertVisible: 'source = Big Buck Bunny' -- assertVisible: 'isPlaying = false' -- assertVisible: 'isAtStart = true' -- assertVisible: 'duration = 596' -- assertVisible: 'currentTime = 0' -- assertVisible: 'mimeType = video/avc' -- assertVisible: 'isSupported = true' -- assertVisible: 'bitratePositive = true' -- assertVisible: 'volume = 1' -- assertVisible: 'status = readyToPlay' -- assertVisible: 'playbackRate = 1' -- assertVisible: 'error = false' - -- tapOn: Play - -- assertVisible: 'isPlaying = true' -- assertVisible: 'isAtStart = false' - -- tapOn: Pause -- tapOn: Seek to 30s -- assertVisible: 'currentTime = 30' -- runFlow: - file: ../_nested-flows/viewshot-comparison.yaml - env: - screenshotOutputPath: 'expo-video' - testID: 'video-view' - mode: 'keep-originals' - -- tapOn: Trigger an Error - -- assertVisible: 'source = No title' -- assertVisible: 'isPlaying = false' - -- assertVisible: 'status = error' -- assertVisible: 'error = true' diff --git a/apps/bare-expo/ios/BareExpo.xcodeproj/project.pbxproj b/apps/bare-expo/ios/BareExpo.xcodeproj/project.pbxproj index a9d9019810f7fa..96f83908ff0631 100644 --- a/apps/bare-expo/ios/BareExpo.xcodeproj/project.pbxproj +++ b/apps/bare-expo/ios/BareExpo.xcodeproj/project.pbxproj @@ -419,14 +419,14 @@ "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXTaskManager/ExpoTaskManager_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoNotifications/ExpoNotifications_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoTaskManager/ExpoTaskManager_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle", @@ -442,14 +442,14 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoMediaLibrary_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle", @@ -497,14 +497,14 @@ "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/EXTaskManager/ExpoTaskManager_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoLocalization/ExpoLocalization_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoMediaLibrary/ExpoMediaLibrary_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoNotifications/ExpoNotifications_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoTaskManager/ExpoTaskManager_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle", @@ -520,14 +520,14 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoLocalization_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoMediaLibrary_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle", diff --git a/apps/bare-expo/ios/Podfile.lock b/apps/bare-expo/ios/Podfile.lock index eaf24c852bc828..eda94b27d4c904 100644 --- a/apps/bare-expo/ios/Podfile.lock +++ b/apps/bare-expo/ios/Podfile.lock @@ -38,11 +38,6 @@ PODS: - EXManifests/Tests (1.0.8): - ExpoModulesCore - ExpoModulesTestCore - - EXNotifications (0.32.11): - - ExpoModulesCore - - EXNotifications/Tests (0.32.11): - - ExpoModulesCore - - ExpoModulesTestCore - Expo (54.0.8): - ExpoModulesCore - hermes-engine @@ -349,6 +344,8 @@ PODS: - ExpoModulesCore - ExpoBrightness (14.0.7): - ExpoModulesCore + - ExpoBrownfield (0.0.1): + - ExpoModulesCore - ExpoCalendar (15.0.7): - ExpoModulesCore - ExpoCamera (17.0.8): @@ -520,6 +517,11 @@ PODS: - React-hermes - ExpoNetwork (8.0.7): - ExpoModulesCore + - ExpoNotifications (0.32.11): + - ExpoModulesCore + - ExpoNotifications/Tests (0.32.11): + - ExpoModulesCore + - ExpoModulesTestCore - ExpoPrint (15.0.7): - ExpoModulesCore - ExpoRouter (6.0.6): @@ -570,6 +572,9 @@ PODS: - ExpoModulesCore - ExpoSystemUI (6.0.7): - ExpoModulesCore + - ExpoTaskManager (14.0.7): + - ExpoModulesCore + - UMAppLoader - ExpoTrackingTransparency (6.0.7): - ExpoModulesCore - ExpoUI (0.2.0-beta.10): @@ -582,9 +587,6 @@ PODS: - ExpoModulesCore - EXStructuredHeaders (5.0.0) - EXStructuredHeaders/Tests (5.0.0) - - EXTaskManager (14.0.7): - - ExpoModulesCore - - UMAppLoader - EXUpdates (29.0.10): - EASClient - EXManifests @@ -2098,7 +2100,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - ReactNativeDependencies - - react-native-keyboard-controller (1.18.5): + - react-native-keyboard-controller (1.20.4): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2110,7 +2112,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-keyboard-controller/common (= 1.18.5) + - react-native-keyboard-controller/common (= 1.20.4) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2121,7 +2123,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-keyboard-controller/common (1.18.5): + - react-native-keyboard-controller/common (1.20.4): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2167,7 +2169,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-safe-area-context (5.6.0): + - react-native-safe-area-context (5.6.2): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2179,8 +2181,8 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-safe-area-context/common (= 5.6.0) - - react-native-safe-area-context/fabric (= 5.6.0) + - react-native-safe-area-context/common (= 5.6.2) + - react-native-safe-area-context/fabric (= 5.6.2) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2191,7 +2193,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-safe-area-context/common (5.6.0): + - react-native-safe-area-context/common (5.6.2): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2213,7 +2215,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-safe-area-context/fabric (5.6.0): + - react-native-safe-area-context/fabric (5.6.2): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2238,7 +2240,7 @@ PODS: - Yoga - react-native-segmented-control (2.5.7): - React-Core - - react-native-skia (2.2.12): + - react-native-skia (2.4.14): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2262,7 +2264,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-slider (5.0.1): + - react-native-slider (5.1.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2274,7 +2276,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-slider/common (= 5.0.1) + - react-native-slider/common (= 5.1.1) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2285,7 +2287,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-slider/common (5.0.1): + - react-native-slider/common (5.1.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2329,7 +2331,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-webview (13.15.0): + - react-native-webview (13.16.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2779,7 +2781,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNCPicker (2.11.2): + - RNCPicker (2.11.4): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2801,7 +2803,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNDateTimePicker (8.4.4): + - RNDateTimePicker (8.6.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2823,7 +2825,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNGestureHandler (2.28.0): + - RNGestureHandler (2.30.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2845,7 +2847,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNReanimated (4.2.0): + - RNReanimated (4.2.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2867,10 +2869,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/reanimated (= 4.2.0) + - RNReanimated/reanimated (= 4.2.1) - RNWorklets - Yoga - - RNReanimated/reanimated (4.2.0): + - RNReanimated/reanimated (4.2.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2892,10 +2894,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/reanimated/apple (= 4.2.0) + - RNReanimated/reanimated/apple (= 4.2.1) - RNWorklets - Yoga - - RNReanimated/reanimated/apple (4.2.0): + - RNReanimated/reanimated/apple (4.2.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2966,7 +2968,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNSVG (15.12.1): + - RNSVG (15.15.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2987,9 +2989,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNSVG/common (= 15.12.1) + - RNSVG/common (= 15.15.1) - Yoga - - RNSVG/common (15.12.1): + - RNSVG/common (15.15.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3111,8 +3113,6 @@ DEPENDENCIES: - EXJSONUtils/Tests (from `../../../packages/expo-json-utils/ios`) - EXManifests (from `../../../packages/expo-manifests/ios`) - EXManifests/Tests (from `../../../packages/expo-manifests/ios`) - - EXNotifications (from `../../../packages/expo-notifications/ios`) - - EXNotifications/Tests (from `../../../packages/expo-notifications/ios`) - Expo (from `../../../packages/expo`) - expo-dev-client (from `../../../packages/expo-dev-client/ios`) - expo-dev-launcher (from `../../../packages/expo-dev-launcher`) @@ -3133,6 +3133,7 @@ DEPENDENCIES: - ExpoBlob (from `../../../packages/expo-blob/ios`) - ExpoBlur (from `../../../packages/expo-blur/ios`) - ExpoBrightness (from `../../../packages/expo-brightness/ios`) + - ExpoBrownfield (from `../../../packages/expo-brownfield/ios`) - ExpoCalendar (from `../../../packages/expo-calendar/ios`) - ExpoCamera (from `../../../packages/expo-camera/ios`) - ExpoCellular (from `../../../packages/expo-cellular/ios`) @@ -3172,6 +3173,8 @@ DEPENDENCIES: - ExpoModulesJSI/Tests (from `../../../packages/expo-modules-core`) - ExpoModulesTestCore (from `../../../packages/expo-modules-test-core/ios`) - ExpoNetwork (from `../../../packages/expo-network/ios`) + - ExpoNotifications (from `../../../packages/expo-notifications/ios`) + - ExpoNotifications/Tests (from `../../../packages/expo-notifications/ios`) - ExpoPrint (from `../../../packages/expo-print/ios`) - ExpoRouter (from `../../../packages/expo-router/ios`) - ExpoScreenCapture (from `../../../packages/expo-screen-capture/ios`) @@ -3186,6 +3189,7 @@ DEPENDENCIES: - ExpoStoreReview (from `../../../packages/expo-store-review/ios`) - ExpoSymbols (from `../../../packages/expo-symbols/ios`) - ExpoSystemUI (from `../../../packages/expo-system-ui/ios`) + - ExpoTaskManager (from `../../../packages/expo-task-manager/ios`) - ExpoTrackingTransparency (from `../../../packages/expo-tracking-transparency/ios`) - ExpoUI (from `../../../packages/expo-ui/ios`) - ExpoVideo (from `../../../packages/expo-video/ios`) @@ -3193,7 +3197,6 @@ DEPENDENCIES: - ExpoWebBrowser (from `../../../packages/expo-web-browser/ios`) - EXStructuredHeaders (from `../../../packages/expo-structured-headers/ios`) - EXStructuredHeaders/Tests (from `../../../packages/expo-structured-headers/ios`) - - EXTaskManager (from `../../../packages/expo-task-manager/ios`) - EXUpdates (from `../../../packages/expo-updates/ios`) - EXUpdates/Tests (from `../../../packages/expo-updates/ios`) - EXUpdatesInterface (from `../../../packages/expo-updates-interface/ios`) @@ -3239,7 +3242,7 @@ DEPENDENCIES: - React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - react-native-keyboard-controller (from `../../../node_modules/react-native-keyboard-controller`) - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" - - react-native-pager-view (from `../../../node_modules/react-native-pager-view`) + - react-native-pager-view (from `../node_modules/react-native-pager-view`) - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - "react-native-segmented-control (from `../../../node_modules/@react-native-segmented-control/segmented-control`)" - "react-native-skia (from `../../../node_modules/@shopify/react-native-skia`)" @@ -3327,9 +3330,6 @@ EXTERNAL SOURCES: EXManifests: inhibit_warnings: false :path: "../../../packages/expo-manifests/ios" - EXNotifications: - inhibit_warnings: false - :path: "../../../packages/expo-notifications/ios" Expo: inhibit_warnings: false :path: "../../../packages/expo" @@ -3377,6 +3377,9 @@ EXTERNAL SOURCES: ExpoBrightness: inhibit_warnings: false :path: "../../../packages/expo-brightness/ios" + ExpoBrownfield: + inhibit_warnings: false + :path: "../../../packages/expo-brownfield/ios" ExpoCalendar: inhibit_warnings: false :path: "../../../packages/expo-calendar/ios" @@ -3478,6 +3481,9 @@ EXTERNAL SOURCES: ExpoNetwork: inhibit_warnings: false :path: "../../../packages/expo-network/ios" + ExpoNotifications: + inhibit_warnings: false + :path: "../../../packages/expo-notifications/ios" ExpoPrint: inhibit_warnings: false :path: "../../../packages/expo-print/ios" @@ -3520,6 +3526,9 @@ EXTERNAL SOURCES: ExpoSystemUI: inhibit_warnings: false :path: "../../../packages/expo-system-ui/ios" + ExpoTaskManager: + inhibit_warnings: false + :path: "../../../packages/expo-task-manager/ios" ExpoTrackingTransparency: inhibit_warnings: false :path: "../../../packages/expo-tracking-transparency/ios" @@ -3538,9 +3547,6 @@ EXTERNAL SOURCES: EXStructuredHeaders: inhibit_warnings: false :path: "../../../packages/expo-structured-headers/ios" - EXTaskManager: - inhibit_warnings: false - :path: "../../../packages/expo-task-manager/ios" EXUpdates: inhibit_warnings: false :path: "../../../packages/expo-updates/ios" @@ -3631,7 +3637,7 @@ EXTERNAL SOURCES: react-native-netinfo: :path: "../../../node_modules/@react-native-community/netinfo" react-native-pager-view: - :path: "../../../node_modules/react-native-pager-view" + :path: "../node_modules/react-native-pager-view" react-native-safe-area-context: :path: "../../../node_modules/react-native-safe-area-context" react-native-segmented-control: @@ -3743,7 +3749,6 @@ SPEC CHECKSUMS: EXConstants: 59d46d25b89f88cc38291a56dbce4d550758f72d EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd EXManifests: 7469efed75d694ce3c43a6da9c6f3886f66d3c26 - EXNotifications: a9eb811deeb51fb8e50ef45569be6ffab3994648 Expo: 9d61298dd8bbd625b62dcd082485fe1692084242 expo-dev-client: b8c16c699a35787e1ac38c6d44ef7552fe3f0334 expo-dev-launcher: 93ab1128548f72825f3fe290ff1c1690114e8def @@ -3760,6 +3765,7 @@ SPEC CHECKSUMS: ExpoBlob: e49626a466984cca4ee809628e171041c2d89bc0 ExpoBlur: b5b7a26572b3c33a11f0b2aa2f95c17c4c393b76 ExpoBrightness: 7c3c86da46b847aadf44401bd28fb6d67050f324 + ExpoBrownfield: 555f04c0a3ad45ffdb7da4a02ada4598768deffb ExpoCalendar: 46ad51c48d2cb1a95439c2215b182428919a0c3d ExpoCamera: 442bbbbd75d73d17f3a972b269efe7595b9b6933 ExpoCellular: 539c6092e755f7b2c37d80f18d9c1f3d7f09e4ab @@ -3772,7 +3778,7 @@ SPEC CHECKSUMS: ExpoFileSystem: adffad7d1f57f768ca7a5e3bf450312fb9ebd27a ExpoFont: d3e56c7cc03d9fd113b90a5513ad32b4bf46b0ff ExpoGL: 39262a8c3d4c36d48a28bc412e3392c25c5391c9 - ExpoGlassEffect: 02d9577dfe05a6b6c9856fd71514120e96792052 + ExpoGlassEffect: 00d7b0bc69a33c5fa86c8a107533f6ff1fabff29 ExpoHaptics: b48d913e7e5f23816c6f130e525c9a6501b160b5 ExpoImage: 0fc185a4a4e462fedfe877ae66204a7c541dfee0 ExpoImageManipulator: d6ebf77518412c40d9837d72bf043fd95edec643 @@ -3791,9 +3797,10 @@ SPEC CHECKSUMS: ExpoMediaLibrary: 648cee3f5dcba13410ec9cc8ac9a426e89a61a31 ExpoMeshGradient: 763087d3b1e6e9a0974e9700ea24cb598816d93c ExpoModulesCore: 22f2efcefda2486b06a0b9f0dcaab8eccdfaf0b4 - ExpoModulesJSI: b5e87deda640f9710d08446db5d110e91db64cc9 + ExpoModulesJSI: c470ea2ed825fce73bdc4ef060c8a22e3f664092 ExpoModulesTestCore: e65555b75a4ed7dd3bcf421ad01d7748bd372c88 ExpoNetwork: 97073786edfe405aba5d0987a544617ed0671ad1 + ExpoNotifications: ce045fa5f6ab4109f04468e04e6fe6ff0427a6d1 ExpoPrint: ad836bb90da10793509651c0ea1f39e120db001e ExpoRouter: 84268c2b41722697cb02cd6c377d588b4f71c9b5 ExpoScreenCapture: 0c785ab7e1fa38943f04b16060d54ae8f886544a @@ -3803,22 +3810,22 @@ SPEC CHECKSUMS: ExpoSharing: 6f52a070c3083c11717cf2236ae8fd6ac45bf028 ExpoSMS: 429edbe5911e7899bc9a0a1a2c106677f552dc33 ExpoSpeech: 0b515130c96898789e72e8d0aac82b7e9123e88b - ExpoSplashScreen: 9d2ff8fa08f2c00336a83f93bebffed3a8312519 + ExpoSplashScreen: 896f624f2e9e98c5de938bf59a62a061f383164d ExpoSQLite: e22c4e298016e658454527ec7dbda5a237b293a3 ExpoStoreReview: 142af4a031b0a7ad99e70e1de8675da03be4ff40 ExpoSymbols: ef7b8ac77ac2d496b1bc3f0f7daf5e19c3a9933a ExpoSystemUI: 5e7fbe4b716edb9e3a7bc0441e4a3b6b1f9eb1ea + ExpoTaskManager: ea6641e9e4df85753f73d90eb5c3286cb052aaee ExpoTrackingTransparency: 92e731b9b9b09353c3ef48e0fc9f82b89a1957c6 ExpoUI: f96af27fddf1a08a8aaf7050e11f8c0ed74db770 ExpoVideo: 7e39a5933892dc640b1b83013f3f9d9d37072151 ExpoVideoThumbnails: c951ae8c6ed9974dd3ae5f79b2dd1d9b6e8ab266 ExpoWebBrowser: 4f0dc52a559027cc0fba6920adc19a7604e2733d EXStructuredHeaders: c951e77f2d936f88637421e9588c976da5827368 - EXTaskManager: 7d80cb59c1faa3dcfa42035b185bcb5209ca7b1b EXUpdates: 83e4d666a085b44149f3b21d5bd057ad37b2c3e5 EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b FBLazyVector: 3a7ea85f6009224ad89f7daeda516f189e6b5759 - hermes-engine: 9a270294aaec03556862ee83edd0f490710bfbb2 + hermes-engine: 6bb3000824be2770010ae038914fa26721255c8e libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -3836,7 +3843,7 @@ SPEC CHECKSUMS: React: 4bc1f928568ad4bcfd147260f907b4ea5873a03b React-callinvoker: 8dd44c31888a314694efd0ef5f19ab7e0e855ef8 React-Core: 0c1f3042e1204c0512b2e4263062c66550d7e6a3 - React-Core-prebuilt: 77dc6b8896f979084f259440ae631ff66ec5caf8 + React-Core-prebuilt: 7894b037a2f0fa699a44de6c88d20acd4235b255 React-CoreModules: f6a626221d52f21b5eb8df2d79780b96f20240e5 React-cxxreact: 2e3990595049d43dd1d59ccd6cb35545f0dc6f03 React-debug: 60be0767f5672afc81bfd6a50d996507430f7906 @@ -3864,15 +3871,15 @@ SPEC CHECKSUMS: React-logger: 4462302b7135d3972e6d229d4b188caa4ffa0f2e React-Mapbuffer: 773eea58ff04e5fc8d525c5216957d6a82a6c84c React-microtasksnativemodule: 0294038bd92e2eb132975258e6ddc0a578a880ca - react-native-keyboard-controller: 5587bad7f102125e2020720c4e37e0b58ff5e52e + react-native-keyboard-controller: 162162ec94d05c4afb903a07d5414c5e719d42a1 react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac react-native-pager-view: e37989fc447aa4b5de6d0d2ffb0229e817329246 - react-native-safe-area-context: d46695b29119ba0871705c55c8ac2868888dcca4 + react-native-safe-area-context: 53f796cb6c814661bbe99fbdfd0585d07b996cdd react-native-segmented-control: 44d14c6899ee12de3384517f4fa1cf4a66ae105c - react-native-skia: 009c318120ef7c6000bf14a6342ad8a89b44070b - react-native-slider: b76fa3dd989d25e8c2751c2a546674dfc0c2b6f8 + react-native-skia: 81f8ef68c66781d41a6a970dd076f91a8d799af5 + react-native-slider: 7814a1258f438c043f370eb82110ef036793e95a react-native-view-shot: 26174e54ec6b4b7c5d70b86964b747919759adc1 - react-native-webview: a4f0775a31b73cf13cfc3d2d2b119aa94ec76e49 + react-native-webview: a0107c12442bf2ac454d509f615daef00f34df47 React-NativeModulesApple: 2097e50465c1e5521d2cc2547e78da227930cf7a React-networking: 25c132be194542cbe1b215963643a0049f2c068a React-oscompat: 759780c1327bb5e50f8e18f41ce40d5ed920abb3 @@ -3906,21 +3913,21 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: bfb12ead469222b022a2024f32aba47ce50de512 ReactCodegen: b9b0ea5c72e425fa5a89a031ada3e64dfd839771 ReactCommon: 0084791d25c4deae3d8b77efd4440fb2d5c38746 - ReactNativeDependencies: c489f425742a360f0c51e45ddbfe43572e754c89 + ReactNativeDependencies: 17a617edb4d5883a4c48339514ccb8b765f8af4f RNCAsyncStorage: e85a99325df9eb0191a6ee2b2a842644c7eb29f4 RNCMaskedView: 3c9d7586e2b9bbab573591dcb823918bc4668005 - RNCPicker: 38a9deb903d9a8ae18b598bac727e6c84b3cf0fa - RNDateTimePicker: 6fdd63f5d1e0f21faf4cc8674957c52958a7efae - RNGestureHandler: 72efe2ab9d5d1b1d25b96f3ad3d174992f3bad87 - RNReanimated: 03e066b9967a8ccd8ff28aa88c098d8117b4d860 + RNCPicker: e0149590451d5eae242cf686014a6f6d808f93c7 + RNDateTimePicker: 5e0666de98f1edfac67ee7dde6be8a5415e487a0 + RNGestureHandler: 8e4a9372425d4caa9e3da5072a8dda7a54ed1097 + RNReanimated: 61462806110686a6f5d7c45c6f910cf73cd57dd9 RNScreens: 08ec2d3a35cbeeb9ddd063e5aa8fb6da5bf3a978 - RNSVG: 595d31b6cd45e92424c6b623bd93e1e9b6aa8b79 + RNSVG: 81c64c70e69ce2a1180a2f7355cada5f8aee001c RNWorklets: 78d1eb5df6aa27c762c1466aaaffba8774d807a8 SDWebImage: f29024626962457f3470184232766516dee8dfea SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 - UMAppLoader: e1234c45d2b7da239e9e90fc4bbeacee12afd5b6 + UMAppLoader: b649e542381c8abe788ffb13893e632d598910a5 Yoga: 35d573dff66536d48493acb65a519482f8219fe8 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 diff --git a/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingBridgeModule.kt b/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingBridgeModule.kt index 8c848a07e2fdb6..f1d026225212db 100644 --- a/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingBridgeModule.kt +++ b/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingBridgeModule.kt @@ -9,8 +9,12 @@ class BenchmarkingBridgeModule : BaseJavaModule() { override fun getName(): String = "BenchmarkingBridgeModule" @ReactMethod(isBlockingSynchronousMethod = true) - fun nothing() { + fun nothing(): Double { // Do nothing + + // For some reason, isBlockingSynchronousMethod doesn't let functions be Void/Unit + // so returning a dummy number + return 0.0 } @ReactMethod(isBlockingSynchronousMethod = true) diff --git a/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingTurboModule.kt b/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingTurboModule.kt index 2141e954808f54..58541b90c5ee3f 100644 --- a/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingTurboModule.kt +++ b/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/BenchmarkingTurboModule.kt @@ -9,8 +9,12 @@ class BenchmarkingTurboModule(reactContext: ReactApplicationContext) : NativeBen return "BenchmarkingTurboModule" } - override fun nothing() { + override fun nothing(): Double { // Do nothing + + // For some reason, isBlockingSynchronousMethod doesn't let functions be Void/Unit + // so returning a dummy number + return 0.0 } override fun addNumbers(a: Double, b: Double): Double { diff --git a/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/NativeBenchmarkingTurboModuleSpec.java b/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/NativeBenchmarkingTurboModuleSpec.java index 164986426f1376..33b6087298f695 100644 --- a/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/NativeBenchmarkingTurboModuleSpec.java +++ b/apps/bare-expo/modules/benchmarking/android/src/main/java/expo/modules/benchmark/NativeBenchmarkingTurboModuleSpec.java @@ -22,7 +22,7 @@ public NativeBenchmarkingTurboModuleSpec(ReactApplicationContext reactContext) { @ReactMethod @DoNotStrip - public abstract void nothing(); + public abstract double nothing(); @ReactMethod(isBlockingSynchronousMethod = true) @DoNotStrip diff --git a/apps/bare-expo/modules/benchmarking/ios/BenchmarkingBridgeModule.mm b/apps/bare-expo/modules/benchmarking/ios/BenchmarkingBridgeModule.mm index c56b5adec6a19d..9371998b0cfeda 100644 --- a/apps/bare-expo/modules/benchmarking/ios/BenchmarkingBridgeModule.mm +++ b/apps/bare-expo/modules/benchmarking/ios/BenchmarkingBridgeModule.mm @@ -4,7 +4,10 @@ @implementation BenchmarkingBridgeModule RCT_EXPORT_MODULE(BenchmarkingBridgeModule); -RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(void, nothing) {} +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(double, nothing) +{ + return 0; +} RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(addNumbers:(double)a b:(double)b) { diff --git a/apps/bare-expo/modules/benchmarking/ios/BenchmarkingTurboModule.mm b/apps/bare-expo/modules/benchmarking/ios/BenchmarkingTurboModule.mm index eaad562e7ffc7d..f7c72edb2b2ac8 100644 --- a/apps/bare-expo/modules/benchmarking/ios/BenchmarkingTurboModule.mm +++ b/apps/bare-expo/modules/benchmarking/ios/BenchmarkingTurboModule.mm @@ -9,7 +9,10 @@ @implementation BenchmarkingTurboModule return std::make_shared(params); } -- (void)nothing {} +- (NSNumber *)nothing +{ + return 0; +} - (NSNumber *)addNumbers:(double)a b:(double)b { diff --git a/apps/bare-expo/modules/benchmarking/src/NativeBenchmarkingTurboModule.ts b/apps/bare-expo/modules/benchmarking/src/NativeBenchmarkingTurboModule.ts index f34e26bace632c..538e5e2c007bee 100644 --- a/apps/bare-expo/modules/benchmarking/src/NativeBenchmarkingTurboModule.ts +++ b/apps/bare-expo/modules/benchmarking/src/NativeBenchmarkingTurboModule.ts @@ -1,7 +1,7 @@ import { TurboModule, TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { - nothing(): void; + nothing(): number; addNumbers(a: number, b: number): number; addStrings(a: string, b: string): string; foldArray(array: number[]): number; diff --git a/apps/bare-expo/package.json b/apps/bare-expo/package.json index 7da8128b17f9e6..1e66b5f41d59e4 100644 --- a/apps/bare-expo/package.json +++ b/apps/bare-expo/package.json @@ -45,15 +45,16 @@ "@expo/dom-webview": "0.2.7", "@expo/styleguide-base": "^1.0.1", "@react-native-async-storage/async-storage": "2.2.0", - "@react-native-community/datetimepicker": "8.4.4", + "@react-native-community/datetimepicker": "8.6.0", "@react-native-community/netinfo": "11.4.1", - "@react-native-community/slider": "5.0.1", + "@react-native-community/slider": "5.1.1", "@react-native-masked-view/masked-view": "0.3.2", - "@react-native-picker/picker": "2.11.2", + "@react-native-picker/picker": "2.11.4", "@react-native-segmented-control/segmented-control": "2.5.7", "@shopify/flash-list": "2.0.2", - "@shopify/react-native-skia": "2.2.12", + "@shopify/react-native-skia": "2.4.14", "expo": "~54.0.8", + "expo-brownfield": "~0.0.1", "expo-build-properties": "~1.0.8", "expo-camera": "~17.0.8", "expo-dev-client": "~6.0.12", @@ -69,15 +70,15 @@ "react-dom": "19.2.0", "react-native": "0.83.1", "react-native-edge-to-edge": "~1.6.1", - "react-native-gesture-handler": "~2.28.0", - "react-native-keyboard-controller": "^1.18.5", + "react-native-gesture-handler": "~2.30.0", + "react-native-keyboard-controller": "^1.20.4", "react-native-pager-view": "6.9.1", - "react-native-reanimated": "4.2.0", - "react-native-safe-area-context": "5.6.0", + "react-native-reanimated": "4.2.1", + "react-native-safe-area-context": "5.6.2", "react-native-screens": "4.19.0", - "react-native-svg": "15.12.1", + "react-native-svg": "15.15.1", "react-native-view-shot": "4.0.3", - "react-native-webview": "13.15.0", + "react-native-webview": "13.16.0", "react-native-worklets": "0.7.1", "test-suite": "*" }, diff --git a/apps/brownfield-tester/.gitignore b/apps/brownfield-tester/.gitignore index 1a81a60de88edb..c66860bb2a5022 100644 --- a/apps/brownfield-tester/.gitignore +++ b/apps/brownfield-tester/.gitignore @@ -69,4 +69,8 @@ buck-out/ .expo/* web-build/ -# @end expo-cli \ No newline at end of file +# @end expo-cli + +# Xcode brownfield frameworks +ios/*.xcframework +ios-integrated/*.xcframework diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester.xcodeproj/project.pbxproj b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester.xcodeproj/project.pbxproj new file mode 100644 index 00000000000000..6c2096a8d6a829 --- /dev/null +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester.xcodeproj/project.pbxproj @@ -0,0 +1,350 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + C0AD1A072F180C2B005A1DBD /* hermes.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0AD1A052F180C2B005A1DBD /* hermes.xcframework */; }; + C0AD1A0E2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0AD1A0D2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + C0AD19F12F180368005A1DBD /* BrownfieldIntegratedTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BrownfieldIntegratedTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C0AD1A052F180C2B005A1DBD /* hermes.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = hermes.xcframework; sourceTree = ""; }; + C0AD1A0D2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = minimaltesterbrownfield.xcframework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + C0AD19F32F180368005A1DBD /* BrownfieldIntegratedTester */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = BrownfieldIntegratedTester; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + C0AD19EE2F180368005A1DBD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C0AD1A0E2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework in Frameworks */, + C0AD1A072F180C2B005A1DBD /* hermes.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + C0AD19E82F180368005A1DBD = { + isa = PBXGroup; + children = ( + C0AD19F32F180368005A1DBD /* BrownfieldIntegratedTester */, + C0AD19F22F180368005A1DBD /* Products */, + C0AD1A052F180C2B005A1DBD /* hermes.xcframework */, + C0AD1A0D2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework */, + ); + sourceTree = ""; + }; + C0AD19F22F180368005A1DBD /* Products */ = { + isa = PBXGroup; + children = ( + C0AD19F12F180368005A1DBD /* BrownfieldIntegratedTester.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + C0AD19F02F180368005A1DBD /* BrownfieldIntegratedTester */ = { + isa = PBXNativeTarget; + buildConfigurationList = C0AD19FC2F18036A005A1DBD /* Build configuration list for PBXNativeTarget "BrownfieldIntegratedTester" */; + buildPhases = ( + C0AD19ED2F180368005A1DBD /* Sources */, + C0AD19EE2F180368005A1DBD /* Frameworks */, + C0AD19EF2F180368005A1DBD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + C0AD19F32F180368005A1DBD /* BrownfieldIntegratedTester */, + ); + name = BrownfieldIntegratedTester; + packageProductDependencies = ( + ); + productName = BrownfieldIntegratedTester; + productReference = C0AD19F12F180368005A1DBD /* BrownfieldIntegratedTester.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + C0AD19E92F180368005A1DBD /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2600; + TargetAttributes = { + C0AD19F02F180368005A1DBD = { + CreatedOnToolsVersion = 26.0; + }; + }; + }; + buildConfigurationList = C0AD19EC2F180368005A1DBD /* Build configuration list for PBXProject "BrownfieldIntegratedTester" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = C0AD19E82F180368005A1DBD; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = C0AD19F22F180368005A1DBD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C0AD19F02F180368005A1DBD /* BrownfieldIntegratedTester */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + C0AD19EF2F180368005A1DBD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C0AD19ED2F180368005A1DBD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C0AD19FA2F18036A005A1DBD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = C8D8QTF339; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + C0AD19FB2F18036A005A1DBD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = C8D8QTF339; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C0AD19FD2F18036A005A1DBD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = C8D8QTF339; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Allow $(PRODUCT_NAME) to access your camera"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.expo.BrownfieldIntegratedTester; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C0AD19FE2F18036A005A1DBD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = C8D8QTF339; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Allow $(PRODUCT_NAME) to access your camera"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.expo.BrownfieldIntegratedTester; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C0AD19EC2F180368005A1DBD /* Build configuration list for PBXProject "BrownfieldIntegratedTester" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0AD19FA2F18036A005A1DBD /* Debug */, + C0AD19FB2F18036A005A1DBD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C0AD19FC2F18036A005A1DBD /* Build configuration list for PBXNativeTarget "BrownfieldIntegratedTester" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0AD19FD2F18036A005A1DBD /* Debug */, + C0AD19FE2F18036A005A1DBD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = C0AD19E92F180368005A1DBD /* Project object */; +} diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000000..eb878970081645 --- /dev/null +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000000..2305880107db52 --- /dev/null +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/Contents.json b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/Contents.json new file mode 100644 index 00000000000000..73c00596a7fca3 --- /dev/null +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldIntegratedTesterApp.swift b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldIntegratedTesterApp.swift new file mode 100644 index 00000000000000..2828d675b7afed --- /dev/null +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldIntegratedTesterApp.swift @@ -0,0 +1,16 @@ +import SwiftUI +import minimaltesterbrownfield + +@main +struct BrownfieldIntegratedTesterApp: App { + @UIApplicationDelegateAdaptor var delegate: BrownfieldAppDelegate + + init() { + ReactNativeHostManager.shared.initialize() + } + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/ContentView.swift b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/ContentView.swift new file mode 100644 index 00000000000000..1696389cb4edc1 --- /dev/null +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/ContentView.swift @@ -0,0 +1,19 @@ +import SwiftUI +import minimaltesterbrownfield + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + ReactNativeView(moduleName: "main") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/apps/expo-go/android/expoview/build.gradle b/apps/expo-go/android/expoview/build.gradle index 03630bb1691fa7..5095b4baf6358f 100644 --- a/apps/expo-go/android/expoview/build.gradle +++ b/apps/expo-go/android/expoview/build.gradle @@ -19,7 +19,8 @@ plugins { alias libs.plugins.android.library alias libs.plugins.kotlin.android alias libs.plugins.download - id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" + id("org.jetbrains.kotlin.plugin.compose") version "2.2.21" + id("org.jetbrains.kotlin.plugin.serialization") version "2.2.21" // Use your Kotlin version } apply plugin: 'kotlin-kapt' apply from: new File(rootDir, "versioning_linking.gradle") @@ -48,6 +49,7 @@ repositories { apply plugin: 'com.facebook.react' +apply plugin: 'org.jetbrains.kotlin.plugin.compose' react { /* Autolinking */ @@ -59,7 +61,8 @@ android { namespace "host.exp.expoview" buildFeatures { - buildConfig = true + buildConfig true + compose true } composeOptions { @@ -165,7 +168,15 @@ dependencies { implementation 'io.coil-kt.coil3:coil-network-okhttp:3.2.0' implementation 'androidx.compose.material3:material3:1.4.0-alpha10' implementation 'androidx.core:core-splashscreen:1.0.1' - + implementation 'io.coil-kt:coil-compose:2.6.0' + implementation "androidx.navigation:navigation-compose:2.8.5" + implementation "androidx.compose.ui:ui:1.7.6" + implementation "androidx.compose.runtime:runtime-saveable:1.7.6" + implementation "androidx.compose.material3:material3:1.3.1" + implementation "androidx.compose.animation:animation:1.7.6" + implementation "androidx.compose.foundation:foundation:1.7.6" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7" testImplementation 'junit:junit:4.13.2' @@ -218,6 +229,7 @@ dependencies { api 'com.google.firebase:firebase-crashlytics-ndk:19.3.0' api "androidx.room:room-runtime:2.1.0" implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.code.gson:gson:2.13.2' api(expoLibs.fresco.animated.gif) api(expoLibs.fresco.animated.webp) @@ -254,10 +266,6 @@ dependencies { api 'com.squareup.okhttp3:okhttp:3.10.0' api 'com.squareup.okhttp3:okhttp-urlconnection:3.10.0' - // expo-av - // See explanation in expo-av/build.gradle - api 'com.google.android.exoplayer:extension-okhttp:2.18.1' - // expo-application api 'com.android.installreferrer:installreferrer:1.0' @@ -272,7 +280,8 @@ dependencies { implementation "androidx.camera:camera-video:${camerax_version}" implementation "androidx.camera:camera-view:${camerax_version}" implementation "androidx.camera:camera-extensions:${camerax_version}" - implementation "com.google.mlkit:barcode-scanning:17.2.0" + implementation "com.google.android.gms:play-services-code-scanner:16.1.0" + implementation "com.google.mlkit:barcode-scanning:17.3.0" implementation 'androidx.camera:camera-mlkit-vision:1.4.0-alpha02' // expo-store-review diff --git a/apps/expo-go/android/expoview/src/main/AndroidManifest.xml b/apps/expo-go/android/expoview/src/main/AndroidManifest.xml index e1aeae7613eb83..3aabde62d459e0 100644 --- a/apps/expo-go/android/expoview/src/main/AndroidManifest.xml +++ b/apps/expo-go/android/expoview/src/main/AndroidManifest.xml @@ -120,6 +120,21 @@ + + + + + + + + + + + diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.kt index 99863356139e3e..1ee88e7d44252d 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExpoApplication.kt @@ -7,6 +7,7 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader +import expo.modules.devmenu.api.DevMenuApi import host.exp.exponent.analytics.EXL import host.exp.exponent.branch.BranchManager import host.exp.exponent.di.NativeModuleDepsProvider @@ -31,6 +32,11 @@ abstract class ExpoApplication : Application() { override fun onCreate() { super.onCreate() + // The performance monitor in Expo Go doesn't require overlay permission. + DevMenuApi.configure( + performanceMonitorNeedsOverlayPermission = false + ) + ExpoViewBuildConfig.DEBUG = isDebug ExpoViewBuildConfig.USE_EMBEDDED_KERNEL = shouldUseEmbeddedKernel() diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt index 88213440925931..a2bd6825545668 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.kt @@ -183,13 +183,6 @@ class ExponentManifest @Inject constructor( const val MANIFEST_NAVIGATION_BAR_APPEARANCE = "barStyle" const val MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR = "backgroundColor" - // Notification - const val MANIFEST_NOTIFICATION_INFO_KEY = "notification" - const val MANIFEST_NOTIFICATION_ICON_URL_KEY = "iconUrl" - const val MANIFEST_NOTIFICATION_COLOR_KEY = "color" - const val MANIFEST_NOTIFICATION_ANDROID_MODE = "androidMode" - const val MANIFEST_NOTIFICATION_ANDROID_COLLAPSED_TITLE = "androidCollapsedTitle" - // Debugging const val MANIFEST_DEBUGGER_HOST_KEY = "debuggerHost" const val MANIFEST_MAIN_MODULE_NAME_KEY = "mainModuleName" diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/AuthInterceptor.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/AuthInterceptor.kt index 5eba843a5e7a21..d62ad36aa439e7 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/AuthInterceptor.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/AuthInterceptor.kt @@ -4,15 +4,11 @@ import com.apollographql.apollo.api.http.HttpRequest import com.apollographql.apollo.api.http.HttpResponse import com.apollographql.apollo.network.http.HttpInterceptor import com.apollographql.apollo.network.http.HttpInterceptorChain +import host.exp.exponent.services.SessionRepository -// TODO(@lukmccall): Replace with actual session management implementation -fun interface SessionManager { - fun getSessionSecret(): String? -} - -class AuthInterceptor(private val sessionManager: SessionManager) : HttpInterceptor { +class AuthInterceptor(private val sessionRepository: SessionRepository) : HttpInterceptor { override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse { - val sessionSecret = sessionManager.getSessionSecret() + val sessionSecret = sessionRepository.getSessionSecret() ?: return chain.proceed(request) val newRequest = request.newBuilder() diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/Paginator.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/Paginator.kt index 27c9aecec82793..b439e81ac16dfe 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/Paginator.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/apollo/Paginator.kt @@ -12,30 +12,31 @@ class Paginator( private val fetch: Query ) { private val _data = MutableStateFlow>(emptyList()) - val data: StateFlow> = _data.asStateFlow() - private var currentOffset = 0 - var isLastPage = false - private set - var isFetching = false - private set + private val _isLastPage = MutableStateFlow(false) + val isLastPage: StateFlow = _isLastPage.asStateFlow() + + private val _isFetching = MutableStateFlow(false) + val isFetching: StateFlow = _isFetching.asStateFlow() + + private var currentOffset = 0 suspend fun loadMore() { - if (isFetching || isLastPage) { + if (_isFetching.value || _isLastPage.value) { return } - isFetching = true + _isFetching.value = true val data = fetch(defaultLimit, currentOffset) + currentOffset += data.size if (data.size < defaultLimit || data.isEmpty()) { - isLastPage = true + _isLastPage.value = true } - currentOffset += data.size _data.update { oldData -> oldData + data } - isFetching = false + _isFetching.value = false } } diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/di/NativeModuleDepsProvider.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/di/NativeModuleDepsProvider.kt index f30fa1dcf949ca..63ef616ebd9e55 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/di/NativeModuleDepsProvider.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/di/NativeModuleDepsProvider.kt @@ -13,6 +13,7 @@ import host.exp.exponent.ExponentManifest import host.exp.exponent.analytics.EXL import host.exp.exponent.kernel.services.ExpoKernelServiceRegistry import host.exp.exponent.network.ExponentNetwork +import host.exp.exponent.services.ExponentHistoryService import host.exp.exponent.storage.ExponentSharedPreferences import kotlinx.coroutines.Dispatchers import java.lang.reflect.Field @@ -47,6 +48,10 @@ class NativeModuleDepsProvider(application: Application) { @DoNotStrip var mKernelServiceRegistry: ExpoKernelServiceRegistry = ExpoKernelServiceRegistry(mContext, mExponentSharedPreferences) + @Inject + @DoNotStrip + var mKernelExponentHistoryService: ExponentHistoryService = ExponentHistoryService(mExponentSharedPreferences) + @Inject @DoNotStrip val mUpdatesDatabaseHolder: DatabaseHolder = DatabaseHolder(UpdatesDatabase.getInstance(mContext, Dispatchers.IO)) diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/BaseExperienceActivity.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/BaseExperienceActivity.kt index dc58947cc0fca1..d1d8a83e1b16d4 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/BaseExperienceActivity.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/BaseExperienceActivity.kt @@ -185,8 +185,6 @@ abstract class BaseExperienceActivity : ReactNativeActivity() { } companion object { - private val TAG = BaseExperienceActivity::class.java.simpleName - // TODO: kill. just use Exponent class's activity var visibleActivity: BaseExperienceActivity? = null private set diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/DetachedModuleRegistryAdapter.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/DetachedModuleRegistryAdapter.kt deleted file mode 100644 index 76c5758ed9b192..00000000000000 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/DetachedModuleRegistryAdapter.kt +++ /dev/null @@ -1,64 +0,0 @@ -package host.exp.exponent.experience - -import com.facebook.react.bridge.NativeModule -import com.facebook.react.bridge.ReactApplicationContext -import expo.modules.adapters.react.ReactModuleRegistryProvider -import expo.modules.core.ModuleRegistry -import expo.modules.core.interfaces.RegistryLifecycleListener -import expo.modules.manifests.core.Manifest -import host.exp.exponent.kernel.ExperienceKey -import host.exp.exponent.utils.ScopedContext -import versioned.host.exp.exponent.modules.universal.ConstantsBinding -import versioned.host.exp.exponent.modules.universal.ExpoModuleRegistryAdapter -import versioned.host.exp.exponent.modules.universal.ScopedUIManagerModuleWrapper - -open class DetachedModuleRegistryAdapter(moduleRegistryProvider: ReactModuleRegistryProvider) : - ExpoModuleRegistryAdapter(moduleRegistryProvider) { - - override fun createNativeModules( - scopedContext: ScopedContext, - experienceKey: ExperienceKey, - experienceProperties: Map, - manifest: Manifest, - otherModules: List - ): List { - val reactApplicationContext = scopedContext.context as ReactApplicationContext - - // We only use React application context, because we're detached -- no scopes - val moduleRegistry = mModuleRegistryProvider[reactApplicationContext] - - moduleRegistry.registerInternalModule( - ConstantsBinding( - scopedContext, - experienceProperties, - manifest - ) - ) - - // ReactAdapterPackage requires ReactContext - val reactContext = scopedContext.context as ReactApplicationContext - for (internalModule in mReactAdapterPackage.createInternalModules(reactContext)) { - moduleRegistry.registerInternalModule(internalModule) - } - - // Overriding ScopedUIManagerModuleWrapper from ReactAdapterPackage - moduleRegistry.registerInternalModule(ScopedUIManagerModuleWrapper(reactContext)) - - // Adding other modules (not universal) to module registry as consumers. - // It allows these modules to refer to universal modules. - for (otherModule in otherModules) { - if (otherModule is RegistryLifecycleListener) { - moduleRegistry.registerExtraListener(otherModule as RegistryLifecycleListener) - } - } - configureModuleRegistry(moduleRegistry, reactApplicationContext) - return getNativeModulesFromModuleRegistry(reactApplicationContext, moduleRegistry, null) - } - - protected open fun configureModuleRegistry( - moduleRegistry: ModuleRegistry, - reactContext: ReactApplicationContext - ) { - // Subclasses may add more modules here. - } -} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/DevMenuSharedPreferencesAdapter.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/DevMenuSharedPreferencesAdapter.kt new file mode 100644 index 00000000000000..7b07595def816b --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/DevMenuSharedPreferencesAdapter.kt @@ -0,0 +1,22 @@ +package host.exp.exponent.experience + +import android.app.Application +import expo.modules.devmenu.DevMenuDefaultPreferences +import expo.modules.devmenu.DevMenuPreferences +import host.exp.exponent.storage.ExponentSharedPreferences + +class DevMenuSharedPreferencesAdapter( + application: Application, + val exponentSharedPreferences: ExponentSharedPreferences, + val devMenuSharedPreferences: DevMenuPreferences = DevMenuDefaultPreferences(application) +) : DevMenuPreferences by devMenuSharedPreferences { + override var isOnboardingFinished: Boolean + get() = exponentSharedPreferences + .getBoolean(ExponentSharedPreferences.ExponentSharedPreferencesKey.IS_ONBOARDING_FINISHED_KEY) + set(value) { + exponentSharedPreferences.setBoolean( + ExponentSharedPreferences.ExponentSharedPreferencesKey.IS_ONBOARDING_FINISHED_KEY, + value + ) + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExperienceActivity.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExperienceActivity.kt index bdd486f9a1d786..6aaa63ddc273e4 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExperienceActivity.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExperienceActivity.kt @@ -51,7 +51,6 @@ import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewProvi import host.exp.exponent.experience.splashscreen.legacy.singletons.SplashScreen import host.exp.exponent.kernel.ExperienceKey import host.exp.exponent.kernel.ExponentUrls -import host.exp.exponent.kernel.Kernel.KernelStartedRunningEvent import host.exp.exponent.kernel.KernelConstants import host.exp.exponent.kernel.KernelConstants.ExperienceOptions import host.exp.exponent.kernel.KernelProvider @@ -297,14 +296,16 @@ open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDele return false } - /** - * Handles key commands. - */ - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - if (reactHost != null && !isCrashed) { - return devMenuFragment?.onKeyUp(keyCode, event) ?: super.onKeyUp(keyCode, event) + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_UP) { + if (reactHost != null && !isCrashed) { + val wasHandled = devMenuFragment?.onKeyUp(event.keyCode, event) + if (wasHandled == true) { + return true + } + } } - return super.onKeyUp(keyCode, event) + return super.dispatchKeyEvent(event) } /** @@ -319,10 +320,6 @@ open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDele } } - fun onEventMainThread(event: KernelStartedRunningEvent?) { - AsyncCondition.notify(KERNEL_STARTED_RUNNING_KEY) - } - override fun onDoneLoading() { reactSurface?.view?.let { setReactRootView(it) @@ -342,7 +339,11 @@ open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDele sdkVersion = manifest?.getExpoGoSDKVersion() ?: "Unknown", engine = "Hermes" ) - } + }, + preferences = DevMenuSharedPreferencesAdapter( + application, + kernel.exponentSharedPreferences + ) ) ) } @@ -778,7 +779,6 @@ open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDele companion object { private val TAG = ExperienceActivity::class.java.simpleName - private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad" const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101 private const val READY_FOR_BUNDLE = "readyForBundle" diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExpoGoReactNativeHost.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExpoGoReactNativeHost.kt index b3b88b71d8975c..c2f7401fa7c0eb 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExpoGoReactNativeHost.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ExpoGoReactNativeHost.kt @@ -102,8 +102,6 @@ class KernelReactNativeHost( ExponentPackage.kernelExponentPackage( application.applicationContext, exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest, - HomeActivity.homeExpoPackages(), - HomeActivity.Companion, data?.initialURL ), ExpoTurboPackage.kernelExpoTurboPackage( diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/HomeActivity.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/HomeActivity.kt index d249dca454c433..f65606946dc3bc 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/HomeActivity.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/HomeActivity.kt @@ -6,55 +6,59 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Bundle -import android.os.Debug -import android.view.View -import android.view.ViewTreeObserver -import android.view.animation.AccelerateInterpolator import androidx.activity.enableEdgeToEdge -import androidx.core.splashscreen.SplashScreen -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.ui.platform.ComposeView +import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader -import de.greenrobot.event.EventBus -import expo.modules.application.ApplicationModule -import expo.modules.asset.AssetModule -import expo.modules.blur.BlurModule -import expo.modules.camera.CameraViewModule -import expo.modules.clipboard.ClipboardModule -import expo.modules.constants.ConstantsModule -import expo.modules.constants.ConstantsPackage -import expo.modules.core.interfaces.Package -import expo.modules.device.DeviceModule -import expo.modules.easclient.EASClientModule -import expo.modules.filesystem.FileSystemModule -import expo.modules.filesystem.legacy.FileSystemLegacyModule -import expo.modules.font.FontLoaderModule -import expo.modules.font.FontUtilsModule -import expo.modules.haptics.HapticsModule -import expo.modules.keepawake.KeepAwakeModule -import expo.modules.kotlin.ModulesProvider -import expo.modules.kotlin.modules.Module -import expo.modules.lineargradient.LinearGradientModule -import expo.modules.notifications.NotificationsPackage -import expo.modules.storereview.StoreReviewModule -import expo.modules.taskManager.TaskManagerPackage -import expo.modules.trackingtransparency.TrackingTransparencyModule -import expo.modules.webbrowser.WebBrowserModule -import host.exp.exponent.Constants import host.exp.exponent.di.NativeModuleDepsProvider -import host.exp.exponent.experience.splashscreen.legacy.SplashScreenModule -import host.exp.exponent.experience.splashscreen.legacy.SplashScreenPackage -import host.exp.exponent.kernel.ExperienceKey -import host.exp.exponent.kernel.Kernel.KernelStartedRunningEvent -import host.exp.exponent.utils.ExperienceActivityUtils +import host.exp.exponent.home.HomeActivityEvent +import host.exp.exponent.home.HomeAppViewModel +import host.exp.exponent.home.HomeAppViewModelFactory +import host.exp.exponent.home.RootNavigation +import host.exp.exponent.home.auth.AuthActivity +import host.exp.exponent.home.auth.AuthResult +import host.exp.exponent.kernel.ExpoViewKernel +import host.exp.exponent.kernel.Kernel +import host.exp.exponent.services.ThemeSetting import host.exp.exponent.utils.ExperienceRTLManager import host.exp.exponent.utils.currentDeviceIsAPhone -import org.json.JSONException +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +open class HomeActivity : AppCompatActivity() { + @Inject + protected lateinit var kernel: Kernel + + val homeActivityEvents = MutableSharedFlow() + + val authLauncher = registerForActivityResult(AuthActivity.Contract()) { result -> + when (result) { + is AuthResult.Success -> { + viewModel.onNewAuthSession(result.sessionSecret) + } + + is AuthResult.Canceled -> {} + } + } + + val viewModel: HomeAppViewModel by viewModels { + HomeAppViewModelFactory( + kernel.exponentHistoryService, + ExpoViewKernel.instance, + homeActivityEvents, + authLauncher + ) + } -open class HomeActivity : BaseExperienceActivity() { //region Activity Lifecycle override fun onCreate(savedInstanceState: Bundle?) { - configureSplashScreen(installSplashScreen()) + NativeModuleDepsProvider.instance.inject(HomeActivity::class.java, this) + enableEdgeToEdge() if (currentDeviceIsAPhone(this)) { @@ -65,134 +69,59 @@ open class HomeActivity : BaseExperienceActivity() { super.onCreate(savedInstanceState) - NativeModuleDepsProvider.instance.inject(HomeActivity::class.java, this) - - manifest = exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest - experienceKey = try { - ExperienceKey.fromManifest(manifest!!) - } catch (e: JSONException) { - ExperienceKey("") - } - - // @sjchmiela, @lukmccall: We are consciously not overriding UI mode in Home, because it has no effect. - // `ExpoAppearanceModule` with which `ExperienceActivityUtils#overrideUiMode` is compatible - // is disabled in Home as of end of 2020, to fix some issues with dev menu, see: - // https://github.com/expo/expo/blob/eb9bd274472e646a730fd535a4bcf360039cbd49/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.java#L200-L207 - // ExperienceActivityUtils.overrideUiMode(mExponentManifest.getKernelManifest(), this); - ExperienceActivityUtils.configureStatusBar( - exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest, - this - ) - - EventBus.getDefault().registerSticky(this) - kernel.startJSKernel(this) + updateStatusBarForTheme(viewModel.selectedTheme.value) ExperienceRTLManager.setRTLPreferences(this, allowRTL = false, forceRTL = false) - } - - override fun shouldCreateLoadingView(): Boolean { - // Home app shouldn't show LoadingView as it indicates state when the app's manifest is being - // downloaded and Splash info is not yet available and this is not the case for Home app - // (Splash info is known from the start). - return false - } - override fun onResume() { - SoLoader.init(this, OpenSourceMergedSoMapping) - super.onResume() - } - //endregion Activity Lifecycle - /** - * This method has been split out from onDestroy lifecycle method to [ReactNativeActivity.destroyReactHost] - * and overridden here as we want to prevent destroying react instance manager when HomeActivity gets destroyed. - * It needs to continue to live since it is needed for DevMenu to work as expected (it relies on ExponentKernelModule from that react context). - */ - override fun destroyReactHost(reason: String) {} - - fun onEventMainThread(event: KernelStartedRunningEvent?) { - reactHost = kernel.reactHost - reactSurface = kernel.surface - - reactHost?.onHostResume(this, this) - reactSurface?.view?.let { - setReactRootView(it) + val contentView = ComposeView(this).apply { + setContent { + RootNavigation(viewModel) + } } - finishLoading() + setContentView(contentView) - if (Constants.DEBUG_COLD_START_METHOD_TRACING) { - Debug.stopMethodTracing() + // Observe theme changes and update status bar accordingly + lifecycleScope.launch { + viewModel.selectedTheme.collect { themeSetting -> + updateStatusBarForTheme(themeSetting) + } } } - private fun configureSplashScreen(customSplashscreen: SplashScreen) { - val contentView = findViewById(android.R.id.content) - val observer = contentView.viewTreeObserver - observer.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - if (isLoading) { - return false - } - contentView.viewTreeObserver.removeOnPreDrawListener(this) - return true + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + val data = intent.data + if (data != null && data.host == "after-delete" && data.scheme == "expauth") { + lifecycleScope.launch { + homeActivityEvents.emit(HomeActivityEvent.AccountDeleted) } - }) - - customSplashscreen.setOnExitAnimationListener { splashScreenViewProvider -> - val splashScreenView = splashScreenViewProvider.view - splashScreenView - .animate() - .setDuration(450) - .alpha(0.0f) - .setInterpolator(AccelerateInterpolator()) - .withEndAction { - splashScreenViewProvider.remove() - }.start() } } - override fun onError(intent: Intent) { - intent.putExtra(ErrorActivity.IS_HOME_KEY, true) - kernel.setHasError() + override fun onResume() { + SoLoader.init(this, OpenSourceMergedSoMapping) + super.onResume() + updateStatusBarForTheme(viewModel.selectedTheme.value) } + //endregion Activity Lifecycle override fun onConfigurationChanged(newConfig: Configuration) { - // Will update the navigation bar colors if the system theme has changed. This is only relevant for the three button navigation bar. - enableEdgeToEdge() super.onConfigurationChanged(newConfig) + enableEdgeToEdge() + updateStatusBarForTheme(viewModel.selectedTheme.value) } - companion object : ModulesProvider { - fun homeExpoPackages(): List { - return listOf( - ConstantsPackage(), - NotificationsPackage(), // home doesn't use notifications, but we want the singleton modules created - TaskManagerPackage(), // load expo-task-manager to restore tasks once the client is opened - SplashScreenPackage() - ) + private fun updateStatusBarForTheme(themeSetting: ThemeSetting) { + val isDarkTheme = when (themeSetting) { + ThemeSetting.Automatic -> + (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + + ThemeSetting.Dark -> true + ThemeSetting.Light -> false } - override fun getModulesMap(): Map, String?> { - return mapOf( - AssetModule::class.java to null, - BlurModule::class.java to null, - CameraViewModule::class.java to null, - ClipboardModule::class.java to null, - ConstantsModule::class.java to null, - DeviceModule::class.java to null, - EASClientModule::class.java to null, - FileSystemModule::class.java to null, - FileSystemLegacyModule::class.java to null, - FontLoaderModule::class.java to null, - FontUtilsModule::class.java to null, - HapticsModule::class.java to null, - KeepAwakeModule::class.java to null, - LinearGradientModule::class.java to null, - SplashScreenModule::class.java to null, - TrackingTransparencyModule::class.java to null, - StoreReviewModule::class.java to null, - WebBrowserModule::class.java to null, - ApplicationModule::class.java to null - ) + WindowCompat.getInsetsController(window, window.decorView).apply { + isAppearanceLightStatusBars = !isDarkTheme } } } diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ReactNativeActivity.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ReactNativeActivity.kt index e877209ff958ce..e9e9dfb970f4f6 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ReactNativeActivity.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/experience/ReactNativeActivity.kt @@ -11,14 +11,12 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.Process -import android.view.KeyEvent import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.core.view.contains -import com.facebook.infer.annotation.Assertions import com.facebook.react.ReactHost import com.facebook.react.bridge.ReactContext.RCTDeviceEventEmitter import com.facebook.react.devsupport.DefaultDevLoadingViewImplementation @@ -240,23 +238,6 @@ abstract class ReactNativeActivity : } // endregion - override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { - devSupportManager?.let { devSupportManager -> - if (!isCrashed && devSupportManager.devSupportEnabled) { - val didDoubleTapR = currentFocus?.let { - Assertions.assertNotNull(doubleTapReloadRecognizer) - .didDoubleTapR(keyCode, it) - } - if (didDoubleTapR == true) { - devSupportManager.reloadExpoApp() - return true - } - } - } - - return super.onKeyUp(keyCode, event) - } - override fun onBackPressed() { if (!isCrashed) { reactHost?.onBackPressed() diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/factories/ExpoGoDevSupportFactory.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/factories/ExpoGoDevSupportFactory.kt index 56f1b70a122b38..86e29b6971af2a 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/factories/ExpoGoDevSupportFactory.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/factories/ExpoGoDevSupportFactory.kt @@ -11,7 +11,7 @@ import com.facebook.react.devsupport.interfaces.DevSupportManager import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager import com.facebook.react.devsupport.interfaces.RedBoxHandler import com.facebook.react.packagerconnection.RequestHandler -import com.facebook.react.devsupport.BridgelessDevSupportManager +import host.exp.exponent.modules.perfmonitor.ExpoBridgelessDevSupportManager import versioned.host.exp.exponent.VersionedUtils class ExpoGoDevSupportFactory(private val devBundleDownloadListener: DevBundleDownloadListener?, private val minNumShakes: Int = 100) : DevSupportManagerFactory { @@ -50,7 +50,7 @@ class ExpoGoDevSupportFactory(private val devBundleDownloadListener: DevBundleDo return ReleaseDevSupportManager() } - return BridgelessDevSupportManager( + return ExpoBridgelessDevSupportManager( applicationContext, reactInstanceManagerHelper, packagerPathForJSBundleName, diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AccountHeaderAction.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AccountHeaderAction.kt new file mode 100644 index 00000000000000..aeb18c1c7823c3 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AccountHeaderAction.kt @@ -0,0 +1,52 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import host.exp.exponent.graphql.fragment.CurrentUserActorData +import host.exp.expoview.R + +@Composable +fun AccountHeaderAction( + account: CurrentUserActorData.Account?, + onLoginClick: () -> Unit = {}, + onAccountClick: () -> Unit = {} +) { + if (account == null) { + OutlinedButton(onClick = onLoginClick) { Text("Log In") } + return + } + + if (account.ownerUserActor == null) { + Icon( + painter = painterResource(R.drawable.account_circle), + contentDescription = "Account icon", + modifier = Modifier + .size(24.dp) + .clip(shape = CircleShape) + .clickable(onClick = onAccountClick) + ) + return + } + + // Show account info + AsyncImage( + model = account.ownerUserActor.profilePhoto, + contentDescription = "Avatar", + modifier = Modifier + .size(24.dp) + .clip(shape = CircleShape) + .clickable(onClick = onAccountClick), + contentScale = ContentScale.Crop + ) +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AccountScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AccountScreen.kt new file mode 100644 index 00000000000000..0f3e1c52e1e39e --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AccountScreen.kt @@ -0,0 +1,161 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import host.exp.exponent.graphql.fragment.CurrentUserActorData +import host.exp.expoview.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountScreen( + viewModel: HomeAppViewModel, + goBack: () -> Unit +) { + val account by viewModel.account.dataFlow.collectAsStateWithLifecycle() + val selectedAccount by viewModel.selectedAccount.collectAsStateWithLifecycle() + + Scaffold( + topBar = { + TopAppBarWithBackIcon("Account", onGoBack = goBack) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(paddingValues) + ) { + LabeledGroup( + label = "Log Out", + modifier = Modifier.padding(top = 8.dp), + wrapWithCard = false + ) { + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + Button( + onClick = { + viewModel.logout() + goBack() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Log Out") + } + } + } + LabeledGroup(label = "Accounts", modifier = Modifier.padding(top = 8.dp)) { + SeparatedList(account?.accounts ?: emptyList()) { item -> + AccountRow( + account = item, + isSelected = item.id == selectedAccount?.id, + onClick = { + viewModel.selectAccount(item.id) + goBack() + } + ) + } + } + } + } +} + +@Composable +private fun AccountRow( + account: CurrentUserActorData.Account, + isSelected: Boolean, + onClick: () -> Unit +) { + val owner = account.ownerUserActor + + @Composable + fun Action() { + if (isSelected) { + Icon( + painter = painterResource(id = R.drawable.check), + contentDescription = "Selected Account", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + } + } + + @Composable + fun Content() { + Column { + val name = owner?.fullName?.takeIf { it.isNotBlank() } + ?: owner?.username + ?: account.name + + Text( + text = name, + fontWeight = FontWeight.SemiBold + ) + + if (owner?.username != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = owner.username, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + ClickableItemRow( + onClick = onClick, + icon = { + if (owner != null) { + AsyncImage( + model = owner.profilePhoto, + contentDescription = "Account icon", + modifier = Modifier + .size(24.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } else { + Icon( + painter = painterResource(R.drawable.account_circle), + contentDescription = "Account icon", + modifier = Modifier.size(24.dp) + ) + } + }, + content = { Content() }, + action = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Action() + } + } + ) +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AppNavHost.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AppNavHost.kt new file mode 100644 index 00000000000000..fa87632dba02dc --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AppNavHost.kt @@ -0,0 +1,296 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import host.exp.expoview.R +import kotlinx.serialization.Serializable + +@Serializable +sealed interface Destination { + @Serializable + object Home : Destination + + @Serializable + object Settings : Destination + + @Serializable + object Projects : Destination + + @Serializable + class ProjectDetails(val appId: String) : Destination + + @Serializable + object Snacks : Destination + + @Serializable + object Account : Destination + + @Serializable + class Branches(val appId: String) : Destination + + @Serializable + class BranchDetails(val branchName: String, val appId: String) : Destination + + @Serializable + object Feedback : Destination +} + +data class BottomBarDestination( + val destination: Destination, + val label: String, + val contentDescription: String, + val icon: Int +) + +val bottomBarDestinations = listOf( + BottomBarDestination( + Destination.Home, + "Home", + "Home", + R.drawable.home + ), + BottomBarDestination( + Destination.Settings, + "Settings", + "Settings", + R.drawable.settings + ) +) + +@Composable +fun RootNavigation( + viewModel: HomeAppViewModel +) { + val navController = rememberNavController() + + val themeSetting by viewModel.selectedTheme.collectAsStateWithLifecycle() + + HomeAppTheme(themeSetting = themeSetting) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + AppNavHost( + navController = navController, + startDestination = Destination.Home, + viewModel = viewModel + ) + } + } +} + +@Composable +fun AppNavHost( + navController: NavHostController, + startDestination: Destination, + viewModel: HomeAppViewModel +) { + val selectedAccount by viewModel.selectedAccount.collectAsStateWithLifecycle() + + @Composable + fun NavAccountHeaderAction() { + AccountHeaderAction( + account = selectedAccount, + onLoginClick = { viewModel.login() }, + onAccountClick = { navController.navigate(Destination.Account) } + ) + Spacer(Modifier.padding(8.dp)) + } + + NavHost( + navController = navController, + startDestination = startDestination + ) { + composable { + HomeScreen( + viewModel = viewModel, + navigateToProjects = { navController.navigate(Destination.Projects) }, + navigateToSnacks = { navController.navigate(Destination.Snacks) }, + onLoginClick = { viewModel.login() }, + navigateToProjectDetails = { appId -> + navController.navigate(Destination.ProjectDetails(appId = appId)) + }, + navigateToFeedback = { + navController.navigate(Destination.Feedback) + }, + bottomBar = { + BottomBar( + navController = navController, + currentDestination = Destination.Home + ) + }, + accountHeader = { NavAccountHeaderAction() } + ) + } + + composable { + SettingsScreen( + viewModel = viewModel, + bottomBar = { + BottomBar( + navController = navController, + currentDestination = Destination.Settings + ) + }, + accountHeader = { NavAccountHeaderAction() } + ) + } + + composable { + ProjectsScreen( + viewModel = viewModel, + onGoBack = { navController.popBackStack() }, + bottomBar = { + BottomBar( + navController = navController, + currentDestination = Destination.Home + ) + }, + navigateToProjectDetails = { appId -> + navController.navigate(Destination.ProjectDetails(appId = appId)) + } + ) + } + + composable { + SnacksScreen( + viewModel = viewModel, + onGoBack = { navController.popBackStack() }, + bottomBar = { + BottomBar( + navController = navController, + currentDestination = Destination.Home + ) + } + ) + } + + composable { + FeedbackScreen( + viewModel = viewModel, + onGoBack = { navController.popBackStack() } + ) + } + + composable { + AccountScreen( + viewModel = viewModel, + goBack = { navController.popBackStack() } + ) + } + + composable { backStackEntry -> + val args = backStackEntry.toRoute() + val appFlow = remember { viewModel.app(args.appId) } + ProjectDetailsScreen( + viewModel = viewModel, + onGoBack = { navController.popBackStack() }, + appFlow = appFlow, + onBranchClick = { branchName -> + navController.navigate( + Destination.BranchDetails( + branchName = branchName, + appId = args.appId + ) + ) + }, + onShowAllBranchesClick = { + navController.navigate(Destination.Branches(appId = args.appId)) + } + ) + } + + composable { backStackEntry -> + val args = backStackEntry.toRoute() + + BranchesScreen( + viewModel = viewModel, + onGoBack = { navController.popBackStack() }, + appId = args.appId, + navigateToBranchDetails = { appId, branchName -> + navController.navigate(Destination.BranchDetails(branchName, appId)) + }, + bottomBar = { + BottomBar( + navController = navController, + currentDestination = Destination.Home + ) + } + ) + } + + composable { backStackEntry -> + val args = backStackEntry.toRoute() + val branchRefreshableFlow = remember(args.branchName, args.appId) { + viewModel.branch(args.branchName, args.appId) + } + + BranchDetailsScreen( + onGoBack = { navController.popBackStack() }, + branchRefreshableFlow = branchRefreshableFlow, + bottomBar = { + BottomBar( + navController = navController, + currentDestination = Destination.Home + ) + } + ) + } + } +} + +@Composable +fun BottomBar( + navController: NavHostController, + currentDestination: Destination +) { + NavigationBar( + windowInsets = NavigationBarDefaults.windowInsets, + containerColor = MaterialTheme.colorScheme.surface + ) { + bottomBarDestinations.forEach { item -> + NavigationBarItem( + selected = currentDestination == item.destination, + onClick = { + navController.navigate(item.destination) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = { + Icon( + painter = painterResource(item.icon), + contentDescription = item.contentDescription + ) + }, + label = { Text(item.label) } + ) + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AppRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AppRow.kt new file mode 100644 index 00000000000000..1ec9ff3e3c3a77 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/AppRow.kt @@ -0,0 +1,44 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import host.exp.exponent.graphql.Home_AccountAppsQuery + +@Composable +fun AppRow( + app: Home_AccountAppsQuery.App, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = app.commonAppData.name, + fontWeight = FontWeight.Medium + ) + Text( + text = app.commonAppData.fullName, + style = MaterialTheme.typography.bodySmall + ) + } + Spacer(modifier = Modifier.width(8.dp)) + } + Spacer(modifier = Modifier.width(8.dp)) +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchDetailsScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchDetailsScreen.kt new file mode 100644 index 00000000000000..6af49572dddd02 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchDetailsScreen.kt @@ -0,0 +1,82 @@ +// In BranchDetailsScreen.kt (or create the file if it doesn't exist) +package host.exp.exponent.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import host.exp.exponent.graphql.BranchDetailsQuery + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BranchDetailsScreen( + onGoBack: () -> Unit, + branchRefreshableFlow: RefreshableFlow, + bottomBar: @Composable () -> Unit = { } +) { + val isRefreshing by branchRefreshableFlow.loadingFlow.collectAsStateWithLifecycle() + val branch by branchRefreshableFlow.dataFlow.collectAsStateWithLifecycle() + val onRefresh = { branchRefreshableFlow.refresh() } + + val pullToRefreshState = rememberPullToRefreshState() + val updates = branch?.updateBranchByName?.updates ?: emptyList() + + Scaffold( + topBar = { + TopAppBarWithBackIcon( + "Branch", + onGoBack = onGoBack + ) + }, + bottomBar = bottomBar + ) { padding -> + PullToRefreshBox( + modifier = Modifier.padding(padding), + state = pullToRefreshState, + isRefreshing = isRefreshing, + onRefresh = onRefresh + ) { + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp) + ) { + Text( + text = branch?.updateBranchByName?.name ?: "Unnamed Branch", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + } + HorizontalDivider() + LabeledGroup(label = "Updates", modifier = Modifier.padding(top = 8.dp)) { + // TODO: Migrate to a LazyColumn if the list of updates can be long + updates.forEachIndexed { index, update -> + UpdateRow(update = update) + if (index < updates.lastIndex) { + HorizontalDivider() + } + } + } + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchRow.kt new file mode 100644 index 00000000000000..f725cad73554c2 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchRow.kt @@ -0,0 +1,47 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import host.exp.exponent.graphql.BranchesForProjectQuery + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun BranchRow( + branch: BranchesForProjectQuery.UpdateBranch, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { +// TODO: Add icons for branch and update + val lastUpdate = branch.updates.lastOrNull() + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Branch: " + branch.name, + // You might want to make the branch name more prominent + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 6.dp) + ) + // If a last update exists, display it using the new UpdateRow composable + if (lastUpdate != null) { + UpdateRow(update = lastUpdate, omitCompatibility = true) + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchesScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchesScreen.kt new file mode 100644 index 00000000000000..bedfc0196aa32c --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/BranchesScreen.kt @@ -0,0 +1,121 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) +@Composable +fun BranchesScreen( + viewModel: HomeAppViewModel, + appId: String, + onGoBack: () -> Unit, + navigateToBranchDetails: (appId: String, branchName: String) -> Unit, + bottomBar: @Composable () -> Unit = {} +) { + val paginatorRefreshableFlow = remember { viewModel.branchesPaginatorRefreshableFlow(appId) } + + val branches by paginatorRefreshableFlow.dataFlow.flatMapLatest { paginator -> + paginator?.data ?: flowOf(emptyList()) + }.collectAsStateWithLifecycle(initialValue = emptyList()) + + val branchesToRender = branches.filter { it.updates.isNotEmpty() } + + val isFetching by paginatorRefreshableFlow.dataFlow.flatMapLatest { paginator -> + paginator?.isFetching ?: flowOf(false) + }.collectAsStateWithLifecycle(initialValue = false) + + val canLoadMore by paginatorRefreshableFlow.dataFlow.flatMapLatest { paginator -> + paginator?.isLastPage?.map { it.not() } ?: flowOf(true) + }.collectAsStateWithLifecycle(initialValue = true) + + val paginator by paginatorRefreshableFlow.dataFlow.collectAsStateWithLifecycle() + + val pullToRefreshState = rememberPullToRefreshState() + val lazyListState = rememberLazyListState() + rememberCoroutineScope() + + Scaffold( + topBar = { + TopAppBarWithBackIcon( + label = "Branches", + onGoBack = onGoBack + ) + }, + bottomBar = bottomBar + ) { padding -> + PullToRefreshBox( + modifier = Modifier.padding(padding), + state = pullToRefreshState, + isRefreshing = isFetching && branches.isNotEmpty(), + onRefresh = { paginator } + ) { + if (isFetching && branches.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyListState + ) { + items(branchesToRender, key = { it.id }) { branch -> + BranchRow( + branch = branch, + onClick = { + navigateToBranchDetails(appId, branch.name) + } + ) + HorizontalDivider() + } + + if (canLoadMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + } + } + + InfiniteListHandler( + listState = lazyListState, + isFetching = isFetching, + canLoadMore = canLoadMore + ) { + paginator?.loadMore() + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ClickableItemRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ClickableItemRow.kt new file mode 100644 index 00000000000000..5e6845ef5fa1a4 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ClickableItemRow.kt @@ -0,0 +1,57 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import expo.modules.devmenu.compose.primitives.Spacer + +typealias ComposableFunction = @Composable () -> Unit + +@Composable +fun ClickableItemRow( + text: String? = null, + icon: ComposableFunction? = null, + paddingVertical: Dp = 16.dp, + paddingHorizontal: Dp = 16.dp, + onClick: () -> Unit, + action: ComposableFunction? = null, + content: ComposableFunction? = null +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding( + vertical = paddingVertical, + horizontal = paddingHorizontal + ), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + icon() + Spacer(modifier = Modifier.width(8.dp)) + } + + if (text != null) { + Text( + text = text, + modifier = Modifier.weight(1f) + ) + } + + content?.invoke() + + if (action != null) { + Spacer(modifier = Modifier.width(8.dp)) + action() + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/CollapsibleItemRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/CollapsibleItemRow.kt new file mode 100644 index 00000000000000..785b22e686fbfa --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/CollapsibleItemRow.kt @@ -0,0 +1,31 @@ +package host.exp.exponent.home + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +@Composable +fun CollapsibleItemRow( + item: @Composable (isExpanded: Boolean, onClick: () -> Unit) -> Unit, + expandedContent: @Composable () -> Unit +) { + var isExpanded by remember { mutableStateOf(false) } + + Column { + item(isExpanded) { isExpanded = !isExpanded } + + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(), + exit = shrinkVertically() + ) { + expandedContent() + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DateTimeUtils.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DateTimeUtils.kt new file mode 100644 index 00000000000000..2e0a59ba785fd9 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DateTimeUtils.kt @@ -0,0 +1,30 @@ +package host.exp.exponent.home + +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +/** + * Parses an ISO 8601 date string and formats it into a human-readable format. + * Example: "2024-05-15T03:03:00.000Z" -> "May 15 2024, 3:03 AM" + * + * @param dateString The ISO 8601 date string from the GraphQL response. + * @return A formatted, readable date-time string, or the original string if parsing fails. + */ +fun formatIsoDateTime(dateString: String?): String { + if (dateString == null) { + return "Unknown date" + } + + return try { + val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) + inputFormat.timeZone = TimeZone.getTimeZone("UTC") + val date = inputFormat.parse(dateString) ?: return dateString + + val outputFormat = SimpleDateFormat("MMM d, yyyy, h:mm a", Locale.ENGLISH) + outputFormat.timeZone = TimeZone.getDefault() + return outputFormat.format(date) + } catch (_: Exception) { + dateString + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DevSessionRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DevSessionRow.kt new file mode 100644 index 00000000000000..65ca1459b91297 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/DevSessionRow.kt @@ -0,0 +1,48 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import host.exp.expoview.R + +@Composable +fun DevSessionRow(session: DevSession) { + val uriHandler = LocalUriHandler.current + val image = if (session.source == DevSessionSource.Desktop) { + painterResource(id = R.drawable.cli) + } else { + painterResource(id = R.drawable.snack) + } + ClickableItemRow( + onClick = { uriHandler.openUri(session.url) }, + icon = { + Image( + painter = image, + contentDescription = "Icon", + modifier = Modifier + .size(24.dp) + .clip(shape = RoundedCornerShape(4.dp)) + ) + } + ) { + Column { + // TODO: Add platform icon + Text( + text = session.description + ) + Text( + text = session.url, + style = MaterialTheme.typography.bodySmall + ) + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/EnterUrlRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/EnterUrlRow.kt new file mode 100644 index 00000000000000..633cbc0409280a --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/EnterUrlRow.kt @@ -0,0 +1,79 @@ +package host.exp.exponent.home + +import android.widget.Toast +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import host.exp.expoview.R + +@Composable +fun EnterUrlRow() { + val textFieldState = rememberTextFieldState(initialText = "") + val uriHandler = LocalUriHandler.current + val context = LocalContext.current + + val connect = { + val urlText = textFieldState.text.toString() + + if (urlText.isNotBlank()) { + val normalized = normalizeUrl(urlText) + runCatching { + uriHandler.openUri(normalized) + }.onFailure { + Toast.makeText(context, "Failed to open URL", Toast.LENGTH_SHORT).show() + } + } + } + + CollapsibleItemRow(item = { isExpanded, onClick -> + ClickableItemRow( + text = "Enter URL", + onClick = onClick, + icon = { + val rotation by animateFloatAsState( + targetValue = if (isExpanded) 90f else 0f, + label = "accordion-arrow" + ) + Icon( + painter = painterResource(id = R.drawable.chevron_right), + contentDescription = "Enter URL icon", + modifier = Modifier + .size(24.dp) + .rotate(rotation) + ) + } + ) + }) { + Column(modifier = Modifier.padding(16.dp)) { + OutlinedTextField( + state = textFieldState, + placeholder = { Text("exp://") }, + modifier = Modifier.fillMaxWidth() + ) + Button( + onClick = connect, + modifier = Modifier + .padding(top = 8.dp) + .fillMaxWidth(), + enabled = textFieldState.text.isNotBlank() + ) { + Text("Connect") + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/FeedbackScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/FeedbackScreen.kt new file mode 100644 index 00000000000000..17f03183600e65 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/FeedbackScreen.kt @@ -0,0 +1,170 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import host.exp.expoview.R + +@Composable +fun FeedbackScreen( + viewModel: HomeAppViewModel = viewModel(), + onGoBack: () -> Unit +) { + val feedbackState by viewModel.feedbackState.collectAsStateWithLifecycle() + var feedback by remember { mutableStateOf("") } + var email by remember { mutableStateOf(viewModel.account.dataFlow.value?.bestContactEmail ?: "") } + + if (feedbackState.isSubmitted) { + SubmittedFeedback(viewModel, onGoBack) + return + } + + Scaffold( + topBar = { + TopAppBarWithBackIcon("Feedback", onGoBack = onGoBack) + } + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .padding(horizontal = 16.dp, vertical = 8.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + "Add your feedback to help us improve this app.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(24.dp)) + + Text( + "Email (optional)", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = email, + onValueChange = { email = it }, + placeholder = { Text("your@email.com") }, + modifier = Modifier.fillMaxWidth(), + enabled = !feedbackState.isSubmitting, + singleLine = true + ) + Spacer(modifier = Modifier.height(16.dp)) + + Text( + "Feedback", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = feedback, + onValueChange = { feedback = it }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + enabled = !feedbackState.isSubmitting + ) + Spacer(modifier = Modifier.fillMaxHeight()) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + feedbackState.error?.let { + Text( + text = "Something went wrong: $it", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 8.dp) + ) + } + Button( + onClick = { viewModel.sendFeedback(feedback, email) }, + enabled = feedback.isNotBlank() && !feedbackState.isSubmitting, + modifier = Modifier.fillMaxWidth() + ) { + if (feedbackState.isSubmitting) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Submit") + } + } + } + } + } +} + +@Composable +private fun SubmittedFeedback( + viewModel: HomeAppViewModel, + onGoBack: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = R.drawable.check), + contentDescription = "Success", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(80.dp) + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = "Thanks for sharing your feedback!", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Your feedback will help us make our app better.", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(24.dp)) + Button(onClick = { + viewModel.resetFeedbackState() + onGoBack() + }, modifier = Modifier.fillMaxWidth()) { + Text("Continue") + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeActivityEvent.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeActivityEvent.kt new file mode 100644 index 00000000000000..6ed0de9eac9978 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeActivityEvent.kt @@ -0,0 +1,5 @@ +package host.exp.exponent.home + +sealed class HomeActivityEvent { + object AccountDeleted : HomeActivityEvent() +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppTheme.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppTheme.kt new file mode 100644 index 00000000000000..02788a261afb00 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppTheme.kt @@ -0,0 +1,72 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import host.exp.exponent.services.ThemeSetting + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp) +) + +private val LightColors = lightColorScheme( + primary = Color(0xFF5A5AD1), + surface = Color(0xFFFFFFFF), + background = Color(0xFFF7F7F7), + onBackground = Color(0xFF1C1B1F), + onSurfaceVariant = Color(0xFF757575) +) + +private val DarkColors = darkColorScheme( + primary = Color(0xFF9EA1FF), + background = Color(0xFF000000), + surface = Color(0xFF161B22), + onSurface = Color(0xFFE6EDF3), + onSurfaceVariant = Color(0xFF8B949E), + outline = Color(0xFF30363D) +) + +@Composable +fun HomeAppTheme( + themeSetting: ThemeSetting, + content: @Composable () -> Unit +) { + val colorScheme = when (themeSetting) { + ThemeSetting.Automatic -> if (isSystemInDarkTheme()) { + DarkColors + } else { + LightColors + } + + ThemeSetting.Dark -> DarkColors + ThemeSetting.Light -> LightColors + } + + MaterialTheme( + colorScheme = colorScheme, + shapes = Shapes, + typography = Typography, + content = content + ) +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppViewModel.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppViewModel.kt new file mode 100644 index 00000000000000..71f43b1befc76d --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeAppViewModel.kt @@ -0,0 +1,626 @@ +package host.exp.exponent.home + +import android.app.Activity +import android.app.Application +import android.content.Context +import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.edit +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.gson.Gson +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning +import host.exp.exponent.analytics.EXL +import host.exp.exponent.apollo.Paginator +import host.exp.exponent.experience.DevMenuSharedPreferencesAdapter +import host.exp.exponent.graphql.BranchDetailsQuery +import host.exp.exponent.graphql.BranchesForProjectQuery +import host.exp.exponent.graphql.Home_AccountAppsQuery +import host.exp.exponent.graphql.Home_AccountSnacksQuery +import host.exp.exponent.graphql.ProjectsQuery +import host.exp.exponent.graphql.fragment.CurrentUserActorData +import host.exp.exponent.home.auth.AuthRequestType +import host.exp.exponent.kernel.ExpoViewKernel +import host.exp.exponent.services.ApolloClientService +import host.exp.exponent.services.ExponentHistoryService +import host.exp.exponent.services.RESTApiClient +import host.exp.exponent.services.SessionRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import okhttp3.OkHttpClient +import java.util.Date +import kotlin.reflect.typeOf +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlin.time.toJavaDuration + +enum class DevSessionPlatform { + Native, + Web +} + +enum class DevSessionSource { + Desktop, + Snack +} + +data class DevSession( + val description: String, + val url: String, + val source: DevSessionSource, + val hostname: String? = null, + val config: Map? = null, + val platform: DevSessionPlatform? = null +) + +data class DevSessionResponse( + val data: List +) + +private const val USER_REVIEW_INFO_PREFS_KEY = "userReviewInfo" + +data class UserReviewState( + val shouldShow: Boolean = false +) + +private data class UserReviewInfo( + val askedForNativeReviewDate: Long? = null, + val lastDismissDate: Long? = null, + val showFeedbackFormDate: Long? = null, + val appOpenedCounter: Int = 0 +) + +data class FeedbackState( + val isSubmitting: Boolean = false, + val isSubmitted: Boolean = false, + val error: String? = null +) + +@Serializable +data class FeedbackBody( + val feedback: String, + val email: String?, + val metadata: Map +) + +fun Int.toJDuration(unit: DurationUnit) = this.toDuration(unit).toJavaDuration() + +class HomeAppViewModelFactory( + private val exponentHistoryService: ExponentHistoryService, + private val expoViewKernel: ExpoViewKernel, + private val homeActivityEvents: MutableSharedFlow, + private val authLauncher: ActivityResultLauncher +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class, extras: CreationExtras): T { + if (modelClass.isAssignableFrom(HomeAppViewModel::class.java)) { + val application = + checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) + return HomeAppViewModel( + application, + exponentHistoryService, + expoViewKernel, + homeActivityEvents, + authLauncher + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} + +class HomeAppViewModel( + application: Application, + private val exponentHistoryService: ExponentHistoryService, + expoViewKernel: ExpoViewKernel, + homeActivityEvents: MutableSharedFlow, + private val authLauncher: ActivityResultLauncher +) : AndroidViewModel(application) { + val userReviewState = MutableStateFlow(UserReviewState()) + + private val userReviewPrefs = application.getSharedPreferences( + USER_REVIEW_INFO_PREFS_KEY, + Context.MODE_PRIVATE + ) + + val devMenuPreferencesAdapter = DevMenuSharedPreferencesAdapter( + application, + exponentHistoryService.exponentSharedPreferences + ) + + private val gson = Gson() + + private var lastCrashDate: Long? = null + + private val client = OkHttpClient + .Builder() + .connectTimeout(10.toJDuration(DurationUnit.SECONDS)) + .readTimeout(10.toJDuration(DurationUnit.SECONDS)) + .writeTimeout(10.toJDuration(DurationUnit.SECONDS)) + .build() + + val recents = exponentHistoryService.history + val sessionRepository = SessionRepository(context = application) + + val expoVersion = expoViewKernel.versionName + + val isDevice = + !(android.os.Build.MODEL.contains("google_sdk") || android.os.Build.MODEL.contains("Emulator")) + + val service = ApolloClientService(client, sessionRepository) + private val restClient = + RESTApiClient(sessionRepository = sessionRepository) + + val account = refreshableFlow( + scope = viewModelScope, + fetcher = { service.currentUser() }, + initialValue = null + ) + + private val selectedAccountId = persistedMutableStateFlow( + scope = viewModelScope, + readValue = { sessionRepository.getSelectedAccountId() }, + writeValue = { value -> + if (value == null) { + sessionRepository.clearSelectedAccountId() + } else { + sessionRepository.saveSelectedAccountId(value) + } + } + ) + + val selectedTheme = persistedMutableStateFlow( + scope = viewModelScope, + readValue = { sessionRepository.getThemeSetting() }, + writeValue = { value -> + sessionRepository.saveThemeSetting(value) + } + ) + + val feedbackState = MutableStateFlow(FeedbackState()) + + val developmentServers: StateFlow> = flow { + while (true) { + try { + val sessions = restClient.sendAuthenticatedApiV2Request( + "development-sessions", + typeOf() + ) + emit(sessions.data) + } catch (_: Exception) { + emit(emptyList()) + } + delay(3000) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) + + fun branch(branchName: String, appId: String): RefreshableFlow { + return refreshableFlow(scope = viewModelScope, fetcher = { + service.branchDetails(branchName, appId) + }, initialValue = null) + } + + fun branches(appId: String?, count: Int): Flow> { + return if (appId == null) { + flow { emit(emptyList()) } + } else { + service.branches(appId, count) + } + } + + val selectedAccount: StateFlow = + selectedAccountId.combine(account.dataFlow) { id, currentUserData -> + if (currentUserData == null) { + return@combine null + } + + if (id == null) { + return@combine currentUserData.accounts.firstOrNull() + } + + currentUserData.accounts.find { it.id == id } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = null + ) + + val apps = refreshableFlow( + scope = viewModelScope, + externalTrigger = selectedAccount, + fetcher = { account -> service.apps(account?.name ?: "", count = 5) }, + initialValue = emptyList() + ) + + val appsPaginatorRefreshableFlow = + refreshableFlow( + scope = viewModelScope, + externalTrigger = selectedAccount, + fetcher = { account -> + flow> { + val paginator = service.apps(account?.name ?: "") + + emit(paginator) + } + }, + initialValue = null + ) + + fun app(appId: String): Flow { + return service.app(appId) + } + + fun branchesPaginatorRefreshableFlow(appId: String): RefreshableFlow?> { + return refreshableFlow( + scope = viewModelScope, + externalTrigger = selectedAccount, + fetcher = { _ -> + flow> { + emit(service.branches(appId = appId)) + } + }, + initialValue = null + ) + } + + val snacks = refreshableFlow( + scope = viewModelScope, + externalTrigger = selectedAccount, + fetcher = { account -> service.snacks(account?.name ?: "", count = 5) }, + initialValue = emptyList() + ) + + fun login() { + authLauncher.launch(AuthRequestType.LOGIN) + } + + fun onNewAuthSession(sessionSecret: String) { + sessionRepository.saveSessionSecret(sessionSecret) + account.refresh() + } + + init { + homeActivityEvents + .onEach { event -> + when (event) { + is HomeActivityEvent.AccountDeleted -> { + logout() + } + } + } + .launchIn(viewModelScope) + lastCrashDate = exponentHistoryService.getLastCrashDate() + + combine( + apps.dataFlow, + snacks.dataFlow + ) { appsList, snacksList -> + updateUserReviewState(appsList.size, snacksList.size) + }.launchIn(viewModelScope) + } + + val snacksPaginatorRefreshableFlow = + refreshableFlow( + scope = viewModelScope, + externalTrigger = selectedAccount, + fetcher = { account -> + flow> { + emit(service.snacks(account?.name ?: "")) + } + }, + initialValue = null + ) + + fun logout() { + sessionRepository.clearSessionSecret() + account.refresh() +// TODO: is there a way to logout browser session too? + } + + fun clearRecents() { + exponentHistoryService.clearHistory() + } + + fun selectAccount(accountId: String?) { + selectedAccountId.value = accountId + } + + fun sendFeedback(feedback: String, email: String) { + viewModelScope.launch { + feedbackState.update { it.copy(isSubmitting = true, error = null) } + try { + if ( + email.isNotBlank() && + !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() + ) { + throw Exception("Please enter a valid email address.") + } + + val metadata = mapOf( + "os" to "${android.os.Build.VERSION.RELEASE}", + "model" to android.os.Build.MODEL, + "expoGoVersion" to expoVersion + ) + + val body = FeedbackBody(feedback, email, metadata) + + restClient.sendUnauthenticatedApiV2Request( + "feedback/expo-go-send", + typeOf(), + body + ) + feedbackState.update { it.copy(isSubmitting = false, isSubmitted = true) } + } catch (e: Exception) { + feedbackState.update { it.copy(isSubmitting = false, error = e.message) } + } + } + } + + fun resetFeedbackState() { + feedbackState.value = FeedbackState() + } + + fun scanQR( + context: Context, + onSuccess: (String) -> Unit, + onError: (String) -> Unit = {} + ) { + val options = GmsBarcodeScannerOptions + .Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + + val scanner = GmsBarcodeScanning.getClient(context, options) + + scanner.startScan() + .addOnSuccessListener { barcode -> + val url = barcode.rawValue ?: run { + onError("No QR code data found") + return@addOnSuccessListener + } + onSuccess(url) + } + .addOnCanceledListener { + onError("QR code scan cancelled") + } + .addOnFailureListener { exception -> + onError("QR code scan failed: ${exception.message ?: "Unknown error"}") + } + } + + /** + * We should only prompt users to review the app if they seem to be + * having a good experience, to check that we verify if the user + * has not experienced any crashes in the last hour, and has at least + * 5 apps or 5 snacks or has opened the app at least 50 times. + * If the user dismisses the review section, we only show it again + * after 15 days. + */ + private suspend fun updateUserReviewState(appsCount: Int, snacksCount: Int) { + val context = getApplication() + + val isStoreReviewAvailable = withContext(Dispatchers.IO) { + if (!isDevice) return@withContext false + try { + ReviewManagerFactory.create(context) + true + } catch (_: Exception) { + false + } + } + + val info = withContext(Dispatchers.IO) { + val json = userReviewPrefs.getString(USER_REVIEW_INFO_PREFS_KEY, null) + json?.let { gson.fromJson(it, UserReviewInfo::class.java) } ?: UserReviewInfo() + } + + val timeNow = Date() + val noRecentCrashes = lastCrashDate?.let { timeNow.time - it > 60 * 60 * 1000 } ?: true + val noRecentDismisses = + info.lastDismissDate?.let { timeNow.time - it > 15L * 24 * 60 * 60 * 1000 } ?: true + + val shouldShow = isStoreReviewAvailable && + info.askedForNativeReviewDate == null && + info.showFeedbackFormDate == null && + noRecentCrashes && + noRecentDismisses && + (info.appOpenedCounter >= 50 || appsCount >= 5 || snacksCount >= 5) + + userReviewState.update { it.copy(shouldShow = shouldShow) } + } + + fun requestStoreReview(activity: Activity) { + updateUserReviewInfo { it.copy(askedForNativeReviewDate = Date().time) } + val manager = ReviewManagerFactory.create(activity) + manager.requestReviewFlow().addOnCompleteListener { task -> + if (task.isSuccessful) { + manager.launchReviewFlow(activity, task.result) + } else { + EXL.e("HomeAppViewModel", "Failed to launch in-app review: ${task.exception?.message}") + } + } + userReviewState.update { it.copy(shouldShow = false) } + } + + fun dismissReviewSection() { + updateUserReviewInfo { it.copy(lastDismissDate = Date().time) } + userReviewState.update { it.copy(shouldShow = false) } + } + + fun provideFeedback() { + updateUserReviewInfo { it.copy(showFeedbackFormDate = Date().time) } + userReviewState.update { it.copy(shouldShow = false) } + // Navigation should be handled in the Composable + } + + private fun updateUserReviewInfo(updateAction: (UserReviewInfo) -> UserReviewInfo) { + viewModelScope.launch(Dispatchers.IO) { + val currentInfo = + userReviewPrefs.getString(USER_REVIEW_INFO_PREFS_KEY, null) + ?.let { gson.fromJson(it, UserReviewInfo::class.java) } + ?: UserReviewInfo() + val newInfo = updateAction(currentInfo) + userReviewPrefs.edit(commit = true) { + putString(USER_REVIEW_INFO_PREFS_KEY, gson.toJson(newInfo)) + } + updateUserReviewState(apps.dataFlow.value.size, snacks.dataFlow.value.size) + } + } +} + +class RefreshableFlow( + val dataFlow: StateFlow, + val loadingFlow: StateFlow, + val refresh: () -> Unit +) + +@OptIn(ExperimentalCoroutinesApi::class) +fun refreshableFlow( + scope: CoroutineScope, + fetcher: () -> Flow, + initialValue: T +): RefreshableFlow { + val refreshTrigger = MutableSharedFlow(replay = 1) + val loadingState = MutableStateFlow(false) + + refreshTrigger.tryEmit(Unit) + + val stateFlow = refreshTrigger + .flatMapLatest { + fetcher() + .onStart { loadingState.value = true } + .onCompletion { loadingState.value = false } + } + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = initialValue + ) + + return RefreshableFlow( + dataFlow = stateFlow, + loadingFlow = loadingState, + refresh = { refreshTrigger.tryEmit(Unit) } + ) +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun refreshableFlow( + scope: CoroutineScope, + externalTrigger: Flow, + initialValue: U, + fetcher: (T) -> Flow +): RefreshableFlow { + val manualRefreshTrigger = MutableSharedFlow(replay = 1) + val loadingState = MutableStateFlow(false) + + val combinedTrigger = externalTrigger.combine( + manualRefreshTrigger.onStart { emit(Unit) } + ) { triggerValue, _ -> + triggerValue + } + + val dataFlow = combinedTrigger + .flatMapLatest { triggerValue -> + fetcher(triggerValue) + .onStart { loadingState.value = true } + .onCompletion { loadingState.value = false } + } + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = initialValue + ) + + return RefreshableFlow( + dataFlow = dataFlow, + loadingFlow = loadingState, + refresh = { manualRefreshTrigger.tryEmit(Unit) } + ) +} + +@OptIn(ExperimentalForInheritanceCoroutinesApi::class) +fun persistedMutableStateFlow( + scope: CoroutineScope, + readValue: () -> T, + writeValue: (T) -> Unit +): MutableStateFlow { + return object : MutableStateFlow { + + private val _state = MutableStateFlow(readValue()) + + override var value: T + get() = _state.value + set(value) { + if (_state.value != value) { + _state.value = value + scope.launch { writeValue(value) } + } + } + + override val subscriptionCount: StateFlow + get() = _state.subscriptionCount + + override fun compareAndSet(expect: T, update: T): Boolean { + val result = _state.compareAndSet(expect, update) + if (result) { + scope.launch { writeValue(update) } + } + return result + } + + override suspend fun emit(value: T) { + _state.emit(value) + writeValue(value) + } + + override fun tryEmit(value: T): Boolean { + val result = _state.tryEmit(value) + if (result) { + scope.launch { writeValue(value) } + } + return result + } + + override val replayCache: List + get() = _state.replayCache + + @ExperimentalCoroutinesApi + override fun resetReplayCache() { + _state.resetReplayCache() + } + + override suspend fun collect(collector: FlowCollector): Nothing { + _state.collect(collector) + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeScreen.kt new file mode 100644 index 00000000000000..88fc6402489ff3 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/HomeScreen.kt @@ -0,0 +1,194 @@ +package host.exp.exponent.home + +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import host.exp.expoview.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + viewModel: HomeAppViewModel, + navigateToProjects: () -> Unit, + navigateToSnacks: () -> Unit, + navigateToProjectDetails: (appId: String) -> Unit, + navigateToFeedback: () -> Unit, + onLoginClick: () -> Unit, + accountHeader: @Composable () -> Unit = { }, + bottomBar: @Composable () -> Unit = { } +) { + val recents by viewModel.recents.collectAsStateWithLifecycle() + val snacks by viewModel.snacks.dataFlow.collectAsStateWithLifecycle() + + val account by viewModel.account.dataFlow.collectAsStateWithLifecycle() + val isRefreshing by viewModel.account.loadingFlow.collectAsStateWithLifecycle() + val apps by viewModel.apps.dataFlow.collectAsStateWithLifecycle() + val developmentServers by viewModel.developmentServers.collectAsStateWithLifecycle() + + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + val state = rememberPullToRefreshState() + val onRefresh: () -> Unit = { + viewModel.account.refresh() + viewModel.apps.refresh() + } + + var showHelpDialog by remember { mutableStateOf(false) } + + if (showHelpDialog) { + AlertDialog( + onDismissRequest = { showHelpDialog = false }, + title = { Text("Troubleshooting") }, + text = { + Text( + stringResource(R.string.help_dialog) + ) + }, + confirmButton = { + TextButton(onClick = { showHelpDialog = false }) { + Text("OK") + } + } + ) + } + + Scaffold( + topBar = { SettingsTopBar(accountHeader = accountHeader) }, + bottomBar = bottomBar + ) { + PullToRefreshBox( + modifier = Modifier.padding(it), + state = state, + isRefreshing = isRefreshing, + onRefresh = onRefresh + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + UpgradeWarning() + UserReviewSection( + viewModel = viewModel, + navigateToFeedback = { navigateToFeedback() } + ) + LabeledGroup( + label = "Development servers", + modifier = Modifier.padding(top = 8.dp), + image = painterResource(id = R.drawable.terminal_icon), + action = { SmallActionButton(label = "HELP", onClick = { showHelpDialog = true }) } + ) { + for (session in developmentServers) { + DevSessionRow(session = session) + HorizontalDivider() + } + if (developmentServers.isEmpty()) { + LocalServerTutorial( + isSignedIn = account != null, + modifier = Modifier.padding(16.dp, 16.dp), + onLoginClick = onLoginClick + ) + HorizontalDivider() + } + EnterUrlRow() + HorizontalDivider() + ClickableItemRow( + text = "Scan QR", + icon = { + Icon( + painter = painterResource(id = R.drawable.qr_code), + contentDescription = "Scan QR Code", + modifier = Modifier.size(24.dp) + ) + }, + onClick = { + viewModel.scanQR( + context, + onSuccess = { url -> + uriHandler.openUri(url) + }, + onError = { error -> + Toast.makeText(context, error, Toast.LENGTH_LONG).show() + } + ) + } + ) + } + + if (!recents.isEmpty()) { + LabeledGroup( + label = "Recent history", + modifier = Modifier.padding(top = 8.dp), + action = { + SmallActionButton( + label = "CLEAR", + onClick = { viewModel.clearRecents() } + ) + } + ) { + for (historyItem in recents) { + RecentRow(historyItem = historyItem) + HorizontalDivider() + } + } + } + + if (!apps.isEmpty()) { + LabeledGroup( + label = "Projects", + modifier = Modifier.padding(top = 8.dp) + ) { + TruncatedList( + apps, + showMoreText = "View all projects", + onShowMoreClick = navigateToProjects + ) { app -> + AppRow(app, onClick = { navigateToProjectDetails(app.commonAppData.id) }) + } + } + } + + if (!snacks.isEmpty()) { + LabeledGroup( + label = "Snacks", + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp) + ) { + TruncatedList( + snacks, + showMoreText = "View all snacks", + onShowMoreClick = navigateToSnacks + ) { snack -> + SnackRow(snack) + } + } + } + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/InfiniteListHandler.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/InfiniteListHandler.kt new file mode 100644 index 00000000000000..f83a96710bcdd7 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/InfiniteListHandler.kt @@ -0,0 +1,52 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.launch + +/** + * A composable that handles the logic for infinite scrolling in a LazyColumn. + * It observes the scroll state and triggers 'onLoadMore' when the user + * scrolls near the end of the list. + * + * @param listState The state of the LazyColumn to observe. + * @param buffer The number of items from the end of the list to start loading more. + * @param isFetching Whether a fetch operation is currently in progress. + * @param canLoadMore Whether there are more items to load. + * @param onLoadMore The suspend function to call when more items should be loaded. + */ +@Composable +fun InfiniteListHandler( + listState: LazyListState, + buffer: Int = 3, + isFetching: Boolean, + canLoadMore: Boolean, + onLoadMore: suspend () -> Unit +) { + val scope = rememberCoroutineScope() + LaunchedEffect(listState) { + snapshotFlow { + val layoutInfo = listState.layoutInfo + val visibleItemsInfo = layoutInfo.visibleItemsInfo + val totalItemCount = layoutInfo.totalItemsCount + + if (visibleItemsInfo.isEmpty()) { + // List is empty, should trigger load + true + } else { + // Check if the last visible item is close to the end + val lastVisibleItem = visibleItemsInfo.last() + lastVisibleItem.index >= totalItemCount - 1 - buffer + } + }.collect { shouldLoadMore -> + if (shouldLoadMore && !isFetching && canLoadMore) { + scope.launch { + onLoadMore() + } + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ItemRowTag.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ItemRowTag.kt new file mode 100644 index 00000000000000..76045f8a4b0f20 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ItemRowTag.kt @@ -0,0 +1,28 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ItemRowTag( + text: String, + modifier: Modifier = Modifier +) { + Surface( + shape = RoundedCornerShape(4.dp), + color = MaterialTheme.colorScheme.background, + modifier = modifier + ) { + Text( + text = text, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + style = MaterialTheme.typography.bodySmall + ) + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/LabeledGroup.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/LabeledGroup.kt new file mode 100644 index 00000000000000..70e12ba4af3161 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/LabeledGroup.kt @@ -0,0 +1,86 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import expo.modules.devmenu.compose.primitives.Spacer + +@Composable +fun LabeledGroup( + modifier: Modifier = Modifier, + label: String? = null, + icon: Painter? = null, + image: Painter? = null, + action: @Composable (() -> Unit)? = null, + wrapWithCard: Boolean = true, + content: @Composable ColumnScope.() -> Unit +) { + Column(modifier = modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 44.dp) + .padding(start = 24.dp, end = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + if (image != null) { + Image( + painter = image, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + if (label != null) { + Text( + text = label, + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.weight(1f) + ) + } + action?.invoke() + } + if (wrapWithCard) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = MaterialTheme.shapes.medium + ), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + content() + } + } else { + content() + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/LocalServerTutorial.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/LocalServerTutorial.kt new file mode 100644 index 00000000000000..25eaf9e0793f40 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/LocalServerTutorial.kt @@ -0,0 +1,46 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LocalServerTutorial( + isSignedIn: Boolean, + onLoginClick: () -> Unit, + modifier: Modifier +) { + Column(modifier = modifier) { + if (isSignedIn) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Start a local development server with:") + OutlinedTextField( + "npx expo start", + onValueChange = { }, + enabled = false, + colors = TextFieldDefaults.colors( + disabledContainerColor = MaterialTheme.colorScheme.background, + disabledTextColor = MaterialTheme.colorScheme.onBackground + ), + modifier = Modifier.fillMaxWidth() + ) + Text("Select the local server when it appears here.") + } + } else { + Text( + "Press here to sign in to your Expo account and see the projects you have recently been working on.", + modifier = Modifier.clickable { + onLoginClick() + } + ) + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectDetailsScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectDetailsScreen.kt new file mode 100644 index 00000000000000..32c57f19fa9e8c --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectDetailsScreen.kt @@ -0,0 +1,122 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import host.exp.exponent.graphql.ProjectsQuery +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalCoroutinesApi::class, + ExperimentalMaterial3ExpressiveApi::class +) +@Composable +fun ProjectDetailsScreen( + viewModel: HomeAppViewModel, + onGoBack: () -> Unit, + onBranchClick: (branchName: String) -> Unit, + onShowAllBranchesClick: () -> Unit, + bottomBar: @Composable () -> Unit = { }, + appFlow: Flow +) { + val app by appFlow.collectAsStateWithLifecycle(initialValue = null) + // Should we use the same flow pattern as in ProjectsScreen? Only case is probably account deletion. + val branches by viewModel + .branches(app?.id, 5) + .collectAsStateWithLifecycle(initialValue = null) + + // Filter the branches to only include those that have updates. + val branchesToRender = branches?.filter { it.updates.isNotEmpty() } + + if (app == null) { + // TODO: show a proper loading state + Text("No app found") + return + } + + Scaffold( + topBar = { + TopAppBarWithBackIcon( + app?.name ?: "Project", + onGoBack = onGoBack + ) + }, + bottomBar = bottomBar + ) { padding -> + // Use a simple Box for the layout, as pull-to-refresh is not needed + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp) + ) { + Text( + text = app?.name ?: "Unnamed Project", + style = MaterialTheme.typography.bodyLargeEmphasized + ) + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = app?.name ?: "Unnamed Project", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Owned by ${app?.ownerAccount?.name}", + style = MaterialTheme.typography.bodySmall + ) + } + HorizontalDivider() + LabeledGroup( + label = "Branches", + modifier = Modifier.padding(top = 8.dp) + ) { + if (branchesToRender?.isNotEmpty() == true) { + TruncatedList( + items = branchesToRender, + maxItems = 3, + onShowMoreClick = { + onShowAllBranchesClick() + }, + renderItem = { branch -> + BranchRow( + branch = branch, + onClick = { onBranchClick(branch.name) } + ) + } + ) + } else { + Text( + text = "No branches found", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(16.dp) + ) + } + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectsScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectsScreen.kt new file mode 100644 index 00000000000000..6803afad02e8f1 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/ProjectsScreen.kt @@ -0,0 +1,118 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) +@Composable +fun ProjectsScreen( + viewModel: HomeAppViewModel, + onGoBack: () -> Unit, + navigateToProjectDetails: (appId: String) -> Unit, + bottomBar: @Composable () -> Unit = { } +) { + val isRefreshing by viewModel + .appsPaginatorRefreshableFlow + .loadingFlow + .collectAsStateWithLifecycle() + + val apps by viewModel + .appsPaginatorRefreshableFlow + .dataFlow + .flatMapLatest { paginator -> + paginator?.data ?: flowOf(emptyList()) + } + .collectAsStateWithLifecycle(initialValue = emptyList()) + + val paginator by viewModel + .appsPaginatorRefreshableFlow + .dataFlow + .collectAsStateWithLifecycle() + val isFetching by viewModel + .appsPaginatorRefreshableFlow + .dataFlow + .flatMapLatest { paginator -> + paginator?.isFetching ?: flowOf(false) + } + .collectAsStateWithLifecycle(initialValue = false) + + val canLoadMore by viewModel + .appsPaginatorRefreshableFlow + .dataFlow + .flatMapLatest { paginator -> + paginator?.isLastPage?.map { it.not() } ?: flowOf(true) + } + .collectAsStateWithLifecycle(initialValue = true) + + val pullToRefreshState = rememberPullToRefreshState() + val lazyListState = rememberLazyListState() + rememberCoroutineScope() + + Scaffold( + topBar = { + TopAppBarWithBackIcon("Projects", onGoBack = onGoBack) + }, + bottomBar = bottomBar + ) { + PullToRefreshBox( + modifier = Modifier.padding(it), + state = pullToRefreshState, +// TODO: find something better than checking apps.isNotEmpty() + isRefreshing = isRefreshing && apps.isNotEmpty(), + onRefresh = { + viewModel.appsPaginatorRefreshableFlow.refresh() + } + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyListState + ) { + items(apps) { app -> + AppRow(app = app, onClick = { navigateToProjectDetails(app.commonAppData.id) }) + HorizontalDivider() + } + if (isFetching) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + } + + InfiniteListHandler( + listState = lazyListState, + isFetching = isFetching, + canLoadMore = canLoadMore, + onLoadMore = { paginator?.loadMore() } + ) + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/RecentRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/RecentRow.kt new file mode 100644 index 00000000000000..478e73407ef977 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/RecentRow.kt @@ -0,0 +1,37 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import host.exp.exponent.services.HistoryItem + +@Composable +fun RecentRow(historyItem: HistoryItem) { + val uriHandler = LocalUriHandler.current + + val iconUrl = historyItem.iconUrl + val name = historyItem.name + + ClickableItemRow( + text = name, + icon = { + AsyncImage( + model = iconUrl, + contentDescription = "Icon for $name", + modifier = Modifier + .size(24.dp) + .clip(shape = RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + }, + onClick = { + uriHandler.openUri(historyItem.manifestUrl) + } + ) +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SeparatedList.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SeparatedList.kt new file mode 100644 index 00000000000000..f91f2972dfd6df --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SeparatedList.kt @@ -0,0 +1,20 @@ +package host.exp.exponent.home + +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable + +@Composable +fun SeparatedList( + items: List, + divider: @Composable () -> Unit = { HorizontalDivider() }, + renderItem: @Composable (T) -> Unit +) { + items.forEachIndexed { index, item -> + renderItem(item) + + // Show divider if it's not the last item in the *displayed* list + if (index < items.lastIndex) { + divider() + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SettingsScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SettingsScreen.kt new file mode 100644 index 00000000000000..263e6c0b9632ec --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SettingsScreen.kt @@ -0,0 +1,388 @@ +package host.exp.exponent.home + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import expo.modules.devmenu.DevMenuPreferences +import host.exp.exponent.generated.ExponentBuildConstants +import host.exp.exponent.services.ThemeSetting +import host.exp.expoview.R +import kotlinx.coroutines.launch + +private fun getMajorVersion(version: String): String { + return version.split(".").firstOrNull() ?: version +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + viewModel: HomeAppViewModel, + bottomBar: @Composable () -> Unit = { }, + accountHeader: @Composable () -> Unit = { } +) { + val selectedTheme by viewModel.selectedTheme.collectAsStateWithLifecycle() + + Scaffold( + topBar = { SettingsTopBar(accountHeader = accountHeader) }, + bottomBar = bottomBar + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(paddingValues) + ) { + ThemeSection( + selectedTheme = selectedTheme, + onThemeSelected = { viewModel.selectedTheme.value = it } + ) + + DeveloperMenuSection(viewModel.devMenuPreferencesAdapter) + + AppInfoSection( + clientVersion = viewModel.expoVersion ?: "unknown", + supportedSdk = getMajorVersion(ExponentBuildConstants.TEMPORARY_SDK_VERSION) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + DeleteAccountSection() + } + } +} + +@Composable +fun ThemeSection( + selectedTheme: ThemeSetting, + onThemeSelected: (ThemeSetting) -> Unit +) { + LabeledGroup(label = "Theme") { + ClickableItemRow( + text = "Automatic", + icon = { + Icon( + painter = painterResource(R.drawable.theme_auto), + contentDescription = "Automatic Theme Icon", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + }, + onClick = { onThemeSelected(ThemeSetting.Automatic) }, + action = { + RadioButton( + selected = selectedTheme == ThemeSetting.Automatic, + onClick = null + ) + } + ) + HorizontalDivider() + ClickableItemRow( + text = "Light", + icon = { + Icon( + painter = painterResource(R.drawable.theme_light), + contentDescription = "Light Theme Icon", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + }, + onClick = { onThemeSelected(ThemeSetting.Light) }, + action = { + RadioButton( + selected = selectedTheme == ThemeSetting.Light, + onClick = null + ) + } + ) + HorizontalDivider() + ClickableItemRow( + text = "Dark", + icon = { + Icon( + painter = painterResource(R.drawable.theme_dark), + contentDescription = "Dark Theme Icon", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + }, + onClick = { onThemeSelected(ThemeSetting.Dark) }, + action = { + RadioButton( + selected = selectedTheme == ThemeSetting.Dark, + onClick = null + ) + } + ) + } +} + +@Composable +fun AppInfoSection( + clientVersion: String, + supportedSdk: String +) { + val context = LocalContext.current + + fun copyToClipboard(label: String, text: String) { + val clipboard = + context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + val clip = android.content.ClipData.newPlainText(label, text) + clipboard.setPrimaryClip(clip) + } + + LabeledGroup(label = "App Info") { + ClickableItemRow( + text = "Client version", + onClick = { copyToClipboard("Client version", clientVersion) }, + action = { + Text( + text = clientVersion, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + ) + HorizontalDivider() + ClickableItemRow( + text = "Supported SDKs", + onClick = { copyToClipboard("Supported SDKs", supportedSdk) }, + action = { + Text( + text = supportedSdk, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + ) + } +} + +@Composable +fun DeveloperMenuSection( + devMenuPreference: DevMenuPreferences +) { + var launchAtStart by remember { mutableStateOf(devMenuPreference.showsAtLaunch) } + var enableShake by remember { mutableStateOf(devMenuPreference.motionGestureEnabled) } + var enableThreeFingerLongPress by remember { mutableStateOf(devMenuPreference.touchGestureEnabled) } + var showFab by remember { mutableStateOf(devMenuPreference.showFab) } + + DisposableEffect(true) { + val onNewPreferences = { + launchAtStart = devMenuPreference.showsAtLaunch + enableShake = devMenuPreference.motionGestureEnabled + enableThreeFingerLongPress = devMenuPreference.touchGestureEnabled + showFab = devMenuPreference.showFab + } + + devMenuPreference.addOnChangeListener(onNewPreferences) + onDispose { + devMenuPreference.removeOnChangeListener(onNewPreferences) + } + } + + @Composable + fun Switch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit + ) { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colorScheme.primary, + checkedTrackColor = MaterialTheme.colorScheme.primaryContainer, + uncheckedThumbColor = MaterialTheme.colorScheme.outline, + uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, + checkedBorderColor = Color.Transparent, + uncheckedBorderColor = Color.Transparent + ) + ) + } + + @Composable + fun SwitchRow( + text: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + iconPainter: Painter + ) { + ClickableItemRow( + text = text, + paddingVertical = 6.dp, + icon = { + Icon( + painter = iconPainter, + contentDescription = "$text icon", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + }, + onClick = { onCheckedChange(!checked) }, + action = { + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } + ) + } + + LabeledGroup(label = "Developer Menu") { + SwitchRow( + text = "Show at launch", + checked = launchAtStart, + onCheckedChange = { newValue -> + launchAtStart = newValue + devMenuPreference.showsAtLaunch = newValue + }, + iconPainter = painterResource(R.drawable.launch_at_start) + ) + + HorizontalDivider() + + SwitchRow( + text = "Shake device", + checked = enableShake, + onCheckedChange = { newValue -> + enableShake = newValue + devMenuPreference.motionGestureEnabled = newValue + }, + iconPainter = painterResource(R.drawable.shake) + ) + + HorizontalDivider() + + SwitchRow( + text = "3 fingers long press", + checked = enableThreeFingerLongPress, + onCheckedChange = { newValue -> + enableThreeFingerLongPress = newValue + devMenuPreference.touchGestureEnabled = newValue + }, + iconPainter = painterResource(R.drawable.three_finger_long_press) + ) + + HorizontalDivider() + + SwitchRow( + text = "Action button", + checked = showFab, + onCheckedChange = { newValue -> + showFab = newValue + devMenuPreference.showFab = newValue + }, + iconPainter = painterResource(R.drawable.fab) + ) + } +} + +@Composable +fun DeleteAccountSection() { + var deletionError by remember { mutableStateOf(null) } + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val handleDeleteAccount: () -> Unit = { + deletionError = null + + coroutineScope.launch { + try { + val redirectBase = "expauth://after-delete" + val encodedRedirect = Uri.encode(redirectBase) + val authSessionURL = + "https://expo.dev/settings/delete-user-expo-go?post_delete_redirect_uri=$encodedRedirect" + + val intent = Intent(Intent.ACTION_VIEW, authSessionURL.toUri()).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } catch (e: Exception) { + deletionError = e.message ?: "An unknown error occurred" + } + } + } + + LabeledGroup(label = "Delete Account") { + Column( + modifier = Modifier + .padding( + start = 16.dp, + top = 16.dp, + end = 16.dp, + bottom = 8.dp + ) + ) { + Text( + text = "This action is irreversible. It will delete your personal account, projects, and activity.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.secondary + ) + + deletionError?.let { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Button( + onClick = handleDeleteAccount, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.error + ), + shape = RoundedCornerShape(4.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.error) + ) { + Text("Delete Account") + } + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SettingsTopBar.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SettingsTopBar.kt new file mode 100644 index 00000000000000..8302df6bee278a --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SettingsTopBar.kt @@ -0,0 +1,35 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import host.exp.expoview.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsTopBar( + accountHeader: @Composable () -> Unit +) { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.big_logo_new_filled), + contentDescription = "Expo Go logo", + modifier = Modifier.size(32.dp, 32.dp) + ) + Text("Expo Go", fontWeight = FontWeight.Bold) + } + }, + actions = { accountHeader() } + ) +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SmallActionButton.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SmallActionButton.kt new file mode 100644 index 00000000000000..3718b0edb758d5 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SmallActionButton.kt @@ -0,0 +1,20 @@ +package host.exp.exponent.home + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +@Composable +fun SmallActionButton( + label: String, + onClick: () -> Unit +) { + TextButton(onClick = onClick) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SnackRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SnackRow.kt new file mode 100644 index 00000000000000..f99f38dca11412 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SnackRow.kt @@ -0,0 +1,108 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import host.exp.exponent.generated.ExponentBuildConstants +import host.exp.exponent.graphql.Home_AccountSnacksQuery + +private fun getMajorVersion(version: String): String { + return version.split(".").firstOrNull() ?: version +} + +private fun isSupportedSdkVersion(sdkVersion: String): Boolean { + val supportedMajor = getMajorVersion(ExponentBuildConstants.TEMPORARY_SDK_VERSION) + val snackMajor = getMajorVersion(sdkVersion) + return supportedMajor == snackMajor +} + +@Composable +fun SnackRow(snack: Home_AccountSnacksQuery.Snack) { + val uriHandler = LocalUriHandler.current + val isSupported = isSupportedSdkVersion(snack.commonSnackData.sdkVersion) + var showUnsupportedDialog by remember { mutableStateOf(false) } + + val handleClick = { + if (isSupported) { + uriHandler.openUri(normalizeSnackUrl(snack.commonSnackData.fullName)) + } else { + showUnsupportedDialog = true + } + } + + if (showUnsupportedDialog) { + val snackMajorVersion = getMajorVersion(snack.commonSnackData.sdkVersion) + val goMajorVersion = getMajorVersion(ExponentBuildConstants.TEMPORARY_SDK_VERSION) + + AlertDialog( + onDismissRequest = { showUnsupportedDialog = false }, + title = { Text("Unsupported SDK ($snackMajorVersion)") }, + text = { Text("The currently running version of Expo Go supports SDK $goMajorVersion only. Update your Snack to this version to run it.") }, + confirmButton = { + TextButton(onClick = { showUnsupportedDialog = false }) { + Text("OK") + } + } + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = handleClick) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + .alpha(if (isSupported) 1f else 0.5f) + ) { + Text( + text = snack.commonSnackData.name, + fontWeight = FontWeight.Medium + ) + if ( + snack.commonSnackData.description.isNotBlank() && + snack.commonSnackData.description != "No description" + ) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = snack.commonSnackData.description, + style = MaterialTheme.typography.bodySmall + ) + } + if (snack.commonSnackData.isDraft || !isSupported) { + Spacer(modifier = Modifier.height(4.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (!isSupported) { + ItemRowTag("Unsupported SDK (${getMajorVersion(snack.commonSnackData.sdkVersion)})") + } + if (snack.commonSnackData.isDraft) { + ItemRowTag("Draft") + } + } + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SnacksScreen.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SnacksScreen.kt new file mode 100644 index 00000000000000..1d21052b1aeb60 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/SnacksScreen.kt @@ -0,0 +1,122 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalCoroutinesApi::class) +@Composable +fun SnacksScreen( + viewModel: HomeAppViewModel, + onGoBack: () -> Unit, + bottomBar: @Composable () -> Unit = {} +) { + val paginator by viewModel + .snacksPaginatorRefreshableFlow + .dataFlow + .collectAsStateWithLifecycle() + + val snacks = paginator + ?.data + ?.collectAsStateWithLifecycle( + initialValue = emptyList() + ) + ?.value + ?: emptyList() + val isFetching by viewModel + .snacksPaginatorRefreshableFlow + .dataFlow + .flatMapLatest { paginator -> + paginator?.isFetching ?: flowOf(false) + } + .collectAsStateWithLifecycle(initialValue = false) + + val canLoadMore by viewModel + .snacksPaginatorRefreshableFlow + .dataFlow + .flatMapLatest { paginator -> + paginator?.isLastPage?.map { it.not() } ?: flowOf(true) + } + .collectAsStateWithLifecycle(initialValue = true) + + val pullToRefreshState = rememberPullToRefreshState() + val lazyListState = rememberLazyListState() + rememberCoroutineScope() + + Scaffold( + topBar = { + TopAppBarWithBackIcon( + label = "Snacks", + onGoBack = onGoBack + ) + }, + bottomBar = bottomBar + ) { padding -> + PullToRefreshBox( + modifier = Modifier.padding(padding), + state = pullToRefreshState, + isRefreshing = isFetching && snacks.isNotEmpty(), + onRefresh = { viewModel.snacksPaginatorRefreshableFlow.refresh() } + ) { + if (isFetching && snacks.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyListState + ) { + items(snacks, key = { it.commonSnackData.id }) { snack -> + SnackRow(snack = snack) + HorizontalDivider() + } + + if (canLoadMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } + } + } + + InfiniteListHandler( + listState = lazyListState, + isFetching = isFetching, + canLoadMore = canLoadMore, + onLoadMore = { paginator?.loadMore() } + ) + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TopAppBarWithBackIcon.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TopAppBarWithBackIcon.kt new file mode 100644 index 00000000000000..bc08da179a21ca --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TopAppBarWithBackIcon.kt @@ -0,0 +1,32 @@ +package host.exp.exponent.home + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import host.exp.expoview.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBarWithBackIcon( + label: String, + onGoBack: () -> Unit +) { + TopAppBar( + navigationIcon = { + IconButton(onClick = onGoBack) { + Icon( + painter = painterResource(id = R.drawable.arrow_back), + contentDescription = "Go back to home" + ) + } + }, + title = { + Text(label, fontWeight = FontWeight.Bold) + } + ) +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TruncatedList.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TruncatedList.kt new file mode 100644 index 00000000000000..e37559d44b75da --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/TruncatedList.kt @@ -0,0 +1,41 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun TruncatedList( + items: List, + maxItems: Int = 3, + showMoreText: String = "View All", + onShowMoreClick: () -> Unit, + renderItem: @Composable (T) -> Unit +) { + val displayItems = items.take(maxItems) + + displayItems.forEachIndexed { index, item -> + renderItem(item) + + if (index < displayItems.lastIndex) { + HorizontalDivider() + } + } + + if (items.size > maxItems) { + HorizontalDivider() + TextButton( + onClick = onShowMoreClick, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Text(showMoreText) + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UpdateRow.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UpdateRow.kt new file mode 100644 index 00000000000000..ed51dd9a25a397 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UpdateRow.kt @@ -0,0 +1,124 @@ +package host.exp.exponent.home + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import host.exp.exponent.generated.ExponentBuildConstants +import host.exp.exponent.graphql.BranchDetailsQuery +import host.exp.exponent.graphql.BranchesForProjectQuery + +private fun isUpdateCompatible(sdkVersion: String?): Boolean { + if (sdkVersion == null) return false + val expoGoMajorVersion = ExponentBuildConstants.TEMPORARY_SDK_VERSION.split(".").firstOrNull() + val updateMajorVersion = sdkVersion.split(".").firstOrNull() + return expoGoMajorVersion != null && expoGoMajorVersion == updateMajorVersion +} + +private fun toExp(httpUrl: String): String { + return try { + val uri = httpUrl.toUri() + uri.buildUpon().scheme("exp").build().toString() + } catch (_: Exception) { + httpUrl + } +} + +private fun openUpdateManifestPermalink( + uriHandler: androidx.compose.ui.platform.UriHandler, + manifestPermalink: String +) { + val expUrl = toExp(manifestPermalink) + uriHandler.openUri(normalizeUrl(expUrl)) +} + +@Composable +private fun UpdateRowContents( + message: String?, + createdAt: String?, + runtimeVersion: String?, + omitCompatibility: Boolean +) { + val isCompatible = isUpdateCompatible(runtimeVersion) + + Text( + text = message ?: "No message", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + maxLines = 2, + modifier = Modifier.padding(bottom = 4.dp) + ) + Text( + text = "Published: " + formatIsoDateTime(createdAt), + style = MaterialTheme.typography.bodySmall + ) + if (!isCompatible && !omitCompatibility) { + ItemRowTag( + text = "Not compatible with this version of Expo Go", + modifier = Modifier.padding(top = 4.dp) + ) + } +} + +@Composable +fun UpdateRow( + update: BranchesForProjectQuery.Update, + omitCompatibility: Boolean = false +) { + val uriHandler = LocalUriHandler.current + val isCompatible = isUpdateCompatible(update.updateData.runtimeVersion) + + val modifier = if (isCompatible) { + Modifier.clickable { + update.updateData.manifestPermalink.let { + openUpdateManifestPermalink(uriHandler, it) + } + } + } else { + Modifier + } + + Column(modifier = modifier) { + UpdateRowContents( + message = update.updateData.message, + createdAt = update.updateData.createdAt as? String, + runtimeVersion = update.updateData.runtimeVersion, + omitCompatibility = omitCompatibility + ) + } +} + +@Composable +fun UpdateRow( + update: BranchDetailsQuery.Update, + omitCompatibility: Boolean = false +) { + val uriHandler = LocalUriHandler.current + val isCompatible = isUpdateCompatible(update.updateData.runtimeVersion) + + val modifier = if (isCompatible) { + Modifier.clickable { + update.updateData.manifestPermalink.let { + openUpdateManifestPermalink(uriHandler, it) + } + } + } else { + Modifier + } + + Column(modifier = modifier.padding(vertical = 8.dp, horizontal = 16.dp)) { + UpdateRowContents( + message = update.updateData.message, + createdAt = update.updateData.createdAt as? String, + runtimeVersion = update.updateData.runtimeVersion, + omitCompatibility = omitCompatibility + ) + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UpgradeWarning.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UpgradeWarning.kt new file mode 100644 index 00000000000000..74b7ab9722f087 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UpgradeWarning.kt @@ -0,0 +1,218 @@ +package host.exp.exponent.home + +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import host.exp.exponent.generated.ExponentBuildConstants +import host.exp.expoview.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.IOException + +private data class VersionsApiResponse( + @SerializedName("sdkVersions") val sdkVersions: Map = emptyMap() +) + +private data class SdkVersionInfo( + @SerializedName("releaseNoteUrl") val releaseNoteUrl: String? = null +) + +private val SDK_VERSION_REGEX = """(\d+)\.\d+\.\d+""".toRegex() + +private val gson = Gson() +private val client = OkHttpClient() + +private val MODAL_DISMISSED_PREF_KEY = "expo_go_upgrade_warning" + +private suspend fun shouldShowUpgradeWarning(context: Context): Pair { +// Don't show on emulators + if (android.os.Build.MODEL.contains("google_sdk") || + android.os.Build.MODEL.contains("Emulator") + ) { + return false to null + } + val request = Request.Builder() + .url("https://api.expo.dev/v2/versions") + .build() + + try { + val response = withContext(Dispatchers.IO) { + client.newCall(request).execute() + } + + if (!response.isSuccessful) return false to null + val responseBody = response.body?.string() ?: return false to null + val data = gson.fromJson(responseBody, VersionsApiResponse::class.java) + + // Extract, sort, and filter SDK versions + val publishedVersions = data.sdkVersions.entries + .mapNotNull { (sdkString, info) -> + SDK_VERSION_REGEX.find(sdkString)?.groupValues?.get(1)?.let { sdk -> + Pair(sdk, info) + } + } + .sortedBy { it.first.toIntOrNull() ?: 0 } + + if (publishedVersions.size < 2) return false to null + + val lastVersion = publishedVersions.last() + val penultimateVersion = publishedVersions[publishedVersions.size - 2] + + val currentGoMajorVersion = ExponentBuildConstants.TEMPORARY_SDK_VERSION.split(".").first() + val currentIsLatestPublished = currentGoMajorVersion == penultimateVersion.first + val latestIsBeta = lastVersion.second.releaseNoteUrl == null + + val shouldShow = currentIsLatestPublished && latestIsBeta + val betaSdkVersion = lastVersion.first + + if (shouldShow) { + val prefs = context.getSharedPreferences(MODAL_DISMISSED_PREF_KEY, Context.MODE_PRIVATE) + val dismissed = prefs.getBoolean("dismissed_$betaSdkVersion", false) + if (dismissed) { + return false to null + } + } + + return shouldShow to betaSdkVersion + } catch (e: IOException) { + return false to null + } +} + +@Composable +fun UpgradeWarning() { + val context = LocalContext.current + var shouldShow by remember { mutableStateOf(false) } + var betaSdkVersion by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + val (show, sdkVersion) = shouldShowUpgradeWarning(context) + shouldShow = show + betaSdkVersion = sdkVersion + } + + if (!shouldShow) { + return + } + + fun dismissWarning() { + shouldShow = false + betaSdkVersion?.let { + context.getSharedPreferences(MODAL_DISMISSED_PREF_KEY, Context.MODE_PRIVATE) + .edit { + putBoolean("dismissed_$it", true) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Surface( + modifier = Modifier.padding(horizontal = 16.dp), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + painter = painterResource(id = R.drawable.warning), + contentDescription = "Warning", + modifier = Modifier + .size(24.dp) + ) + Text( + "New Expo Go version coming soon!", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .weight(1f) + .padding(start = 8.dp) + ) + IconButton(onClick = { dismissWarning() }, modifier = Modifier.size(24.dp)) { + Icon(painter = painterResource(id = R.drawable.close), contentDescription = "Dismiss") + } + } + + val warningText = buildAnnotatedString { + append("A new version of Expo Go will be released to the store soon, and it will ") + withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { + append("only support SDK $betaSdkVersion") + } + append(".") + } + Text(warningText, style = MaterialTheme.typography.bodySmall) + + Message() + } + } +} + +@Composable +private fun Message() { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + "If you have automatic updates enabled for this app, we recommend disabling it to avoid disruption.", + style = MaterialTheme.typography.bodySmall + ) + val textPart2 = buildAnnotatedString { + append("If you ever need to open a project from an earlier SDK version, install the ") + pushLink(LinkAnnotation.Url("https://expo.dev/go")) + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.SemiBold + ) + ) { + append("compatible version") + } + pop() + append(" of Expo Go.") + } + + Text( + text = textPart2, + style = MaterialTheme.typography.bodySmall + ) + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UrlUtils.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UrlUtils.kt new file mode 100644 index 00000000000000..74ac2670aa25e8 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UrlUtils.kt @@ -0,0 +1,54 @@ +package host.exp.exponent.home + +import android.net.Uri +import androidx.core.net.toUri +import host.exp.exponent.generated.ExponentBuildConstants + +// Hardcoded Snack runtime URLs โ€“ these should ideally be fetched from an npm package, but we'll need to keep them in sync. +private const val EXPO_HOST = "expo.dev" +private const val SNACK_RUNTIME_URL_PROTOCOL = "exp" +private const val SNACK_RUNTIME_URL_ENDPOINT = "u.expo.dev/933fd9c0-1666-11e7-afca-d980795c5824" + +fun normalizeSnackUrl( + fullName: String, + channelName: String? = null +): String { + val builder = Uri.Builder() + .scheme(SNACK_RUNTIME_URL_PROTOCOL) + .encodedAuthority(SNACK_RUNTIME_URL_ENDPOINT) + .appendQueryParameter("runtime-version", "exposdk:${ExponentBuildConstants.TEMPORARY_SDK_VERSION}") + .appendQueryParameter("channel-name", "production") + .appendQueryParameter("snack", fullName) + + // Add the channel parameter only if it's provided + channelName?.let { + builder.appendQueryParameter("snack-channel", it) + } + + return builder.build().toString() +} + +/** + * Rewrites a raw URL string into a normalized Expo URL (exp://). + * + * @param rawUrl The user-provided URL string. + * @return A normalized URL string, defaulting to the exp:// protocol. + */ +fun normalizeUrl(rawUrl: String): String { + val trimmedUrl = rawUrl.trim() + var parsedUri = trimmedUrl.toUri() + + if ((parsedUri.scheme != null && parsedUri.authority == null) || (parsedUri.host == null && parsedUri.scheme == null)) { + if (trimmedUrl.startsWith("@")) { + return "exp://$EXPO_HOST/$trimmedUrl" + } else { + parsedUri = "exp://$trimmedUrl".toUri() + } + } + + if (parsedUri.scheme == null) { + return "exp://$trimmedUrl" + } + + return parsedUri.toString() +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UserReviewSection.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UserReviewSection.kt new file mode 100644 index 00000000000000..a2015b667e411c --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/UserReviewSection.kt @@ -0,0 +1,118 @@ +package host.exp.exponent.home + +import android.app.Activity +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import host.exp.expoview.R + +@Composable +fun UserReviewSection( + viewModel: HomeAppViewModel = viewModel(), + navigateToFeedback: () -> Unit +) { + val userReviewState by viewModel.userReviewState.collectAsState() + val context = LocalContext.current + val activity = context as? Activity + + if (!userReviewState.shouldShow || !viewModel.isDevice) { + return + } + + Spacer(modifier = Modifier.height(16.dp)) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enjoying Expo Go?", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f) + ) + IconButton( + onClick = { viewModel.dismissReviewSection() }, + modifier = Modifier.size(24.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.close), + contentDescription = "Dismiss" + ) + } + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Whether you love the app or feel we could be doing better, let us know! Your feedback will help us improve.", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 12.dp) + ) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Button( + onClick = { + viewModel.provideFeedback() + navigateToFeedback() + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Not really", + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + Button( + onClick = { + if (activity != null) { + viewModel.requestStoreReview(activity) + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Love it!", + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthActivity.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthActivity.kt new file mode 100644 index 00000000000000..4ef1ac27731068 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthActivity.kt @@ -0,0 +1,139 @@ +package host.exp.exponent.home.auth + +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_VIEW +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContract +import androidx.appcompat.app.AppCompatActivity +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import java.net.URLEncoder + +private const val SESSION_SECRET_KEY = "session_secret" +private const val AUTH_REQUEST_TYPE_KEY = "auth_request_type" + +private const val WEBSITE_ORIGIN = "https://expo.dev" + +private const val REDIRECT_SCHEME = "expauth" +private const val REDIRECT_HOST = "auth" +private const val REDIRECT_BASE = "$REDIRECT_SCHEME://$REDIRECT_HOST" + +class AuthActivity : AppCompatActivity() { + class Contract : ActivityResultContract() { + override fun createIntent( + context: Context, + input: AuthRequestType + ): Intent { + return Intent(context, AuthActivity::class.java) + .apply { + action = ACTION_VIEW + putExtra(AUTH_REQUEST_TYPE_KEY, input.type) + } + } + + override fun parseResult( + resultCode: Int, + intent: Intent? + ): AuthResult { + if (resultCode == RESULT_CANCELED || intent == null) { + return AuthResult.Canceled + } + + val sessionSecret = intent.getStringExtra(SESSION_SECRET_KEY) ?: return AuthResult.Canceled + + return AuthResult.Success( + sessionSecret = sessionSecret + ) + } + } + + private var wasStarted = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val authRequestType = intent.getStringExtra(AUTH_REQUEST_TYPE_KEY) + ?: throw IllegalStateException("AuthActivity started without AuthRequestType extra") + + wasStarted = true + openWebBrowserAsync( + startUrl = createAuthUrl(AuthRequestType.fromString(authRequestType)) + ) + } + + override fun onResume() { + super.onResume() + + // onNewIntent will handle the response from the web browser + if (intent?.data?.host == REDIRECT_HOST) { + return + } + + // We just open the browser + if (wasStarted) { + wasStarted = false + return + } + + val resultIntent = Intent() + setResult(RESULT_CANCELED, resultIntent) + finish() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + if ( + intent.action == ACTION_VIEW && + intent.data?.scheme == REDIRECT_SCHEME && + intent.data?.host == REDIRECT_HOST + ) { + val sessionSecret = intent.data?.getQueryParameter(SESSION_SECRET_KEY) + + if (sessionSecret.isNullOrEmpty()) { + setResult(RESULT_CANCELED) + finish() + return + } + + val resultIntent = Intent() + .apply { + putExtra(SESSION_SECRET_KEY, sessionSecret) + } + + setResult(RESULT_OK, resultIntent) + finish() + } + } + + private fun openWebBrowserAsync(startUrl: String) { + val intent = createCustomTabsIntent().apply { + data = startUrl.toUri() + } + + startActivity(intent) + } + + private fun createCustomTabsIntent(): Intent { + val builder = CustomTabsIntent.Builder() + builder.setShowTitle(false) + + return builder + .build() + .intent + .apply { + // We cannot use builder's method enableUrlBarHiding, because there is no corresponding disable method and some browsers enables it by default. + putExtra(CustomTabsIntent.EXTRA_ENABLE_URLBAR_HIDING, false) + } + } + + private fun createAuthUrl(type: AuthRequestType): String { + return "${WEBSITE_ORIGIN}/${type.type}?confirm_account=1&app_redirect_uri=${ + URLEncoder.encode( + REDIRECT_BASE, + "UTF-8" + ) + }" + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthRequestType.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthRequestType.kt new file mode 100644 index 00000000000000..c7628ffac44a17 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthRequestType.kt @@ -0,0 +1,12 @@ +package host.exp.exponent.home.auth + +enum class AuthRequestType(internal val type: String) { + LOGIN("login"), + SIGNUP("signup"); + + companion object { + fun fromString(type: String): AuthRequestType { + return entries.firstOrNull { it.type == type } ?: LOGIN + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthResult.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthResult.kt new file mode 100644 index 00000000000000..c4b51336bd6bef --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/home/auth/AuthResult.kt @@ -0,0 +1,6 @@ +package host.exp.exponent.home.auth + +sealed interface AuthResult { + data class Success(val sessionSecret: String) : AuthResult + object Canceled : AuthResult +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/kernel/Kernel.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/kernel/Kernel.kt index 7a1a091bd8c209..364568eb6ec504 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/kernel/Kernel.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/kernel/Kernel.kt @@ -19,8 +19,6 @@ import androidx.core.os.bundleOf import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.ReactHost import com.facebook.react.ReactNativeHost -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.ReadableMap import com.facebook.react.interfaces.fabric.ReactSurface import com.facebook.react.runtime.ReactHostImpl import com.facebook.react.runtime.ReactSurfaceImpl @@ -32,9 +30,14 @@ import expo.modules.manifests.core.ExpoUpdatesManifest import expo.modules.manifests.core.Manifest import expo.modules.notifications.service.NotificationsService.Companion.getNotificationResponseFromOpenIntent import expo.modules.notifications.service.delegates.ExpoHandlingDelegate -import host.exp.exponent.* +import host.exp.exponent.Constants +import host.exp.exponent.ExpoUpdatesAppLoader import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus +import host.exp.exponent.ExponentManifest +import host.exp.exponent.LauncherActivity +import host.exp.exponent.RNObject +import host.exp.exponent.ReactNativeStaticHelpers import host.exp.exponent.analytics.EXL import host.exp.exponent.di.NativeModuleDepsProvider import host.exp.exponent.exceptions.ExceptionUtils @@ -48,8 +51,6 @@ import host.exp.exponent.experience.KernelReactNativeHost import host.exp.exponent.factories.ReactHostFactory import host.exp.exponent.headless.InternalHeadlessAppLoader import host.exp.exponent.kernel.ExponentErrorMessage.Companion.developerErrorMessage -import host.exp.exponent.kernel.ExponentKernelModuleProvider.KernelEventCallback -import host.exp.exponent.kernel.ExponentKernelModuleProvider.queueEvent import host.exp.exponent.kernel.ExponentUrls.toHttp import host.exp.exponent.kernel.KernelConstants.ExperienceOptions import host.exp.exponent.network.ExponentNetwork @@ -57,6 +58,8 @@ import host.exp.exponent.notifications.ExponentNotification import host.exp.exponent.notifications.ExponentNotificationManager import host.exp.exponent.notifications.NotificationActionCenter import host.exp.exponent.notifications.ScopedNotificationsUtils +import host.exp.exponent.services.ExponentHistoryService +import host.exp.exponent.services.HistoryItem import host.exp.exponent.storage.ExponentDB import host.exp.exponent.storage.ExponentSharedPreferences import host.exp.exponent.utils.AsyncCondition @@ -72,7 +75,6 @@ import kotlinx.coroutines.withContext import org.json.JSONException import org.json.JSONObject import java.lang.ref.WeakReference -import java.util.* import javax.inject.Inject // TOOD: need to figure out when we should reload the kernel js. Do we do it every time you visit @@ -109,6 +111,9 @@ class Kernel : KernelInterface() { @Inject lateinit var exponentNetwork: ExponentNetwork + @Inject + lateinit var exponentHistoryService: ExponentHistoryService + var activityContext: Activity? = null set(value) { if (value != null) { @@ -324,7 +329,11 @@ class Kernel : KernelInterface() { val surface: ReactSurface get() { - val surface = ReactSurfaceImpl.createWithView(context, KernelConstants.HOME_MODULE_NAME, kernelLaunchOptions) + val surface = ReactSurfaceImpl.createWithView( + context, + KernelConstants.HOME_MODULE_NAME, + kernelLaunchOptions + ) surface.attach(reactHost as ReactHostImpl) surface.start() return surface @@ -693,25 +702,11 @@ class Kernel : KernelInterface() { if (existingTask == null) { sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl) } - val params = Arguments.createMap().apply { - putString("manifestUrl", manifestUrl) - putString("manifestString", manifest.toString()) - } - queueEvent( - "ExponentKernel.addHistoryItem", - params, - object : KernelEventCallback { - override fun onEventSuccess(result: ReadableMap) { - EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.") - } - - override fun onEventFailure(errorMessage: String?) { - EXL.e( - TAG, - "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage" - ) - } - } + exponentHistoryService.addHistoryItem( + HistoryItem( + manifestUrl = manifestUrl, + manifest = manifest + ) ) killOrphanedLauncherActivities() } diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/ExpoBridgelessDevSupportManager.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/ExpoBridgelessDevSupportManager.kt new file mode 100644 index 00000000000000..01856592325515 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/ExpoBridgelessDevSupportManager.kt @@ -0,0 +1,65 @@ +package host.exp.exponent.modules.perfmonitor + +import android.content.Context +import com.facebook.react.bridge.ReactContext +import com.facebook.react.common.SurfaceDelegateFactory +import com.facebook.react.devsupport.BridgelessDevSupportManager +import com.facebook.react.devsupport.ReactInstanceDevHelper +import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener +import com.facebook.react.devsupport.interfaces.DevLoadingViewManager +import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager +import com.facebook.react.devsupport.interfaces.RedBoxHandler +import com.facebook.react.packagerconnection.RequestHandler + +internal class ExpoBridgelessDevSupportManager( + applicationContext: Context, + reactInstanceManagerHelper: ReactInstanceDevHelper, + packagerPathForJSBundleName: String?, + enableOnCreate: Boolean, + redBoxHandler: RedBoxHandler?, + devBundleDownloadListener: DevBundleDownloadListener?, + minNumShakes: Int, + customPackagerCommandHandlers: Map?, + surfaceDelegateFactory: SurfaceDelegateFactory?, + devLoadingViewManager: DevLoadingViewManager?, + pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager? +) : + BridgelessDevSupportManager( + applicationContext, + reactInstanceManagerHelper, + packagerPathForJSBundleName, + enableOnCreate, + redBoxHandler, + devBundleDownloadListener, + minNumShakes, + customPackagerCommandHandlers, + surfaceDelegateFactory, + devLoadingViewManager, + pausedInDebuggerOverlayManager + ) { + + private val perfController = PerfMonitorController(applicationContext) { + devSettings.isFpsDebugEnabled = false + } + + override fun setFpsDebugEnabled(isFpsDebugEnabled: Boolean) { + devSettings.isFpsDebugEnabled = isFpsDebugEnabled + perfController.syncEnabledState( + isFpsDebugEnabled, + currentReactContext + ) + } + + override fun onNewReactContextCreated(reactContext: ReactContext) { + super.onNewReactContextCreated(reactContext) + perfController.onContextCreated(reactContext) + if (devSettings.isFpsDebugEnabled) { + perfController.enable(reactContext) + } + } + + override fun onReactInstanceDestroyed(reactContext: ReactContext) { + super.onReactInstanceDestroyed(reactContext) + perfController.onContextDestroyed(reactContext) + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorController.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorController.kt new file mode 100644 index 00000000000000..ffb382a8c0c2bd --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorController.kt @@ -0,0 +1,80 @@ +package host.exp.exponent.modules.perfmonitor + +import android.content.Context +import com.facebook.react.bridge.ReactContext + +internal class PerfMonitorController( + context: Context, + private val onDisableRequested: () -> Unit +) { + private val dataSource = PerfMonitorDataSource() + private val overlay = PerfMonitorOverlay(context, dataSource) { + disable() + onDisableRequested() + } + private var currentContext: ReactContext? = null + private var enabled = false + private var dataSourceRunning = false + + fun enable(reactContext: ReactContext?) { + currentContext = reactContext + enabled = true + overlay.setReactContext(reactContext) + + if (!overlay.isShowing()) { + try { + overlay.show() + } catch (_: Throwable) { + enabled = false + onDisableRequested.invoke() + return + } + } + maybeStartDataSource() + } + + fun disable() { + enabled = false + if (dataSourceRunning) { + dataSource.stop() + dataSourceRunning = false + } + overlay.hide() + } + + fun onContextCreated(reactContext: ReactContext) { + currentContext = reactContext + maybeStartDataSource() + } + + fun onContextDestroyed(reactContext: ReactContext) { + if (currentContext == reactContext) { + currentContext = null + if (dataSourceRunning) { + dataSource.stop() + dataSourceRunning = false + } + } + } + + fun syncEnabledState(isEnabled: Boolean, context: ReactContext?) { + if (isEnabled) { + enable(context ?: currentContext) + } else { + disable() + } + } + + fun isEnabled() = enabled + + private fun maybeStartDataSource() { + if (!enabled) { + return + } + val reactContext = currentContext ?: return + if (!dataSourceRunning) { + dataSource.start(reactContext) + dataSourceRunning = true + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorDataSource.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorDataSource.kt new file mode 100644 index 00000000000000..42946ff9c5ea46 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorDataSource.kt @@ -0,0 +1,281 @@ +package host.exp.exponent.modules.perfmonitor + +import android.os.Handler +import android.os.Looper +import android.view.Choreographer +import androidx.annotation.AnyThread +import androidx.annotation.MainThread +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReactMarker +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.fabric.DevToolsReactPerfLogger +import com.facebook.react.fabric.FabricUIManager +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.common.UIManagerType +import kotlin.math.max +import kotlin.math.min + +/** + * Collects runtime metrics (UI/JS FPS, RSS, Hermes heap placeholder, Fabric layout duration) + */ +internal class PerfMonitorDataSource() : DevToolsReactPerfLogger.DevToolsReactPerfLoggerListener { + data class Track( + val label: String, + val currentFps: Int, + val history: List + ) + + data class Snapshot( + val uiTrack: Track, + val jsTrack: Track, + val rssMB: Double, + val layoutDurationMs: Double + ) + + interface Listener { + @MainThread + fun onSnapshot(snapshot: Snapshot) + } + + private val listeners = mutableListOf() + private val uiBuffer = FpsBuffer("UI", FpsBuffer.TrackType.UI) + private val jsBuffer = FpsBuffer("JS", FpsBuffer.TrackType.JS) + private val handler = Handler(Looper.getMainLooper()) + + private var fpsRunnable: Runnable? = null + private var fabricListenerAttached = false + private var layoutDurationMs = 0.0 + private var reactContext: ReactContext? = null + val uiManager: FabricUIManager? + get() { + val reactContext = reactContext ?: return null + return UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC) as? FabricUIManager + } + + fun addListener(listener: Listener) { + listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + @AnyThread + fun start(reactContext: ReactContext) { + UiThreadUtil.runOnUiThread { + if (fpsRunnable != null) { + return@runOnUiThread + } + this.reactContext = reactContext + uiBuffer.start(reactContext) + jsBuffer.start(reactContext) + attachFabricListenerIfNeeded(reactContext) + scheduleNextTick() + } + } + + @AnyThread + fun stop() { + UiThreadUtil.runOnUiThread { + handler.removeCallbacksAndMessages(null) + fpsRunnable = null + uiBuffer.stop() + jsBuffer.stop() + detachFabricListenerIfNeeded() + reactContext = null + } + } + + override fun onFabricCommitEnd(commitPoint: DevToolsReactPerfLogger.FabricCommitPoint) { + val duration = commitPoint.layoutDuration + if (duration > 0) { + layoutDurationMs = duration.toDouble() + } + } + + private fun scheduleNextTick() { + handler.removeCallbacksAndMessages(null) + val runnable = Runnable { + val uiTrack = uiBuffer.collect() + val jsTrack = jsBuffer.collect() + val rss = readRss() + + val snapshot = Snapshot( + uiTrack = uiTrack, + jsTrack = jsTrack, + rssMB = rss, + layoutDurationMs = layoutDurationMs + ) + listeners.forEach { it.onSnapshot(snapshot) } + scheduleNextTick() + } + fpsRunnable = runnable + handler.postDelayed(runnable, SAMPLE_INTERVAL_MS) + } + + private fun readRss(): Double { + val runtime = Runtime.getRuntime() + val javaHeapUsed = (runtime.totalMemory() - runtime.freeMemory()) / 1024.0 / 1024.0 + val nativeHeap = android.os.Debug.getNativeHeapAllocatedSize() / 1024.0 / 1024.0 + return javaHeapUsed + nativeHeap + } + + private fun attachFabricListenerIfNeeded(reactContext: ReactContext) { + if (fabricListenerAttached) return + val uiManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC) + if (uiManager is FabricUIManager) { + var logger = uiManager.mDevToolsReactPerfLogger + + if (logger == null) { + logger = DevToolsReactPerfLogger() + uiManager.mDevToolsReactPerfLogger = logger + ReactMarker.addFabricListener(logger) + } + + logger.addDevToolsReactPerfLoggerListener(this) + fabricListenerAttached = true + } + } + + private fun detachFabricListenerIfNeeded() { + if (!fabricListenerAttached) return + val reactContext = reactContext ?: return + val uiManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC) + if (uiManager is FabricUIManager) { + uiManager.mDevToolsReactPerfLogger?.removeDevToolsReactPerfLoggerListener(this) + } + fabricListenerAttached = false + } + + private class FpsBuffer( + private val label: String, + private val trackType: TrackType + ) { + enum class TrackType { UI, JS } + + private var choreographerCallback: ChoreographerFpsTracker? = null + private val history = ArrayDeque() + + fun start(reactContext: ReactContext) { + stop() + choreographerCallback = ChoreographerFpsTracker(trackType, reactContext).also { + it.start() + } + history.clear() + } + + fun collect(): Track { + val tracker = choreographerCallback ?: return Track(label, 0, emptyList()) + val fps = tracker.getFpsAndReset() + if (history.size >= HISTORY_LENGTH) { + history.removeFirst() + } + history.addLast(fps) + val clamped = history.map { sample -> min(max(sample, 0.0), MAX_FPS) } + return Track(label, fps.toInt(), clamped) + } + + fun stop() { + choreographerCallback?.stop() + choreographerCallback = null + history.clear() + } + + companion object { + private const val HISTORY_LENGTH = 10 + private const val MAX_FPS = 120.0 + } + } + + /** + * This replaces FpsDebugFrameCallback. + * For UI tracking: runs on main thread's Choreographer + * For JS tracking: runs on JS thread's Choreographer (if available, otherwise falls back to main) + */ + private class ChoreographerFpsTracker( + private val trackType: FpsBuffer.TrackType, + private val reactContext: ReactContext + ) { + private var frameCount = 0 + private var startTimeNanos = 0L + private var isRunning = false + + @Volatile + private var choreographer: Choreographer? = null + + private val frameCallback = object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + if (!isRunning) { + return + } + frameCount++ + choreographer?.postFrameCallback(this) + } + } + + fun start() { + if (isRunning) { + return + } + isRunning = true + frameCount = 0 + startTimeNanos = System.nanoTime() + + when (trackType) { + FpsBuffer.TrackType.UI -> { + UiThreadUtil.runOnUiThread { + choreographer = Choreographer.getInstance() + choreographer?.postFrameCallback(frameCallback) + } + } + + FpsBuffer.TrackType.JS -> { + try { + reactContext.runOnJSQueueThread { + try { + choreographer = Choreographer.getInstance() + choreographer?.postFrameCallback(frameCallback) + } catch (_: Throwable) { + isRunning = false + } + } + } catch (_: Throwable) { + isRunning = false + } + } + } + } + + fun stop() { + if (!isRunning) { + return + } + isRunning = false + choreographer?.removeFrameCallback(frameCallback) + choreographer = null + } + + fun getFpsAndReset(): Double { + if (!isRunning) { + return 0.0 + } + + val currentTimeNanos = System.nanoTime() + val elapsedSeconds = (currentTimeNanos - startTimeNanos) / 1_000_000_000.0 + val fps = if (elapsedSeconds > 0) { + frameCount / elapsedSeconds + } else { + 0.0 + } + + frameCount = 0 + startTimeNanos = currentTimeNanos + + return fps + } + } + + companion object { + private const val SAMPLE_INTERVAL_MS = 1_000L + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorOverlay.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorOverlay.kt new file mode 100644 index 00000000000000..7261eded9772fc --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/modules/perfmonitor/PerfMonitorOverlay.kt @@ -0,0 +1,433 @@ +package host.exp.exponent.modules.perfmonitor + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.MotionEvent +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.facebook.react.bridge.ReactContext +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +internal class PerfMonitorOverlay( + private val context: Context, + private val dataSource: PerfMonitorDataSource, + private val onClose: () -> Unit +) : PerfMonitorDataSource.Listener { + + private val lifecycleOwner = OverlayLifecycleOwner() + private val composeView = ComposeView(context) + private var snapshot by mutableStateOf(null) + private var isShowing = false + private var lastX = 0f + private var lastY = 0f + private var isDragging = false + private var reactContext: ReactContext? = null + private val mainHandler = Handler(Looper.getMainLooper()) + + private val dragThreshold = 20 * context.resources.displayMetrics.density + private val container = object : FrameLayout(context) { + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + lastX = event.rawX + lastY = event.rawY + isDragging = false + return false + } + + MotionEvent.ACTION_MOVE -> { + val dx = event.rawX - lastX + val dy = event.rawY - lastY + + if (!isDragging && (abs(dx) > dragThreshold || abs(dy) > dragThreshold)) { + isDragging = true + return true + } + return isDragging + } + } + return false + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_MOVE -> { + if (isDragging) { + val dx = event.rawX - lastX + val dy = event.rawY - lastY + x += dx + y += dy + lastX = event.rawX + lastY = event.rawY + return true + } + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + isDragging = false + return true + } + } + return super.onTouchEvent(event) + } + } + + init { + composeView.setViewTreeLifecycleOwner(lifecycleOwner) + composeView.setViewTreeSavedStateRegistryOwner(lifecycleOwner) + + composeView.setContent { + MaterialTheme(colorScheme = perfMonitorColorScheme) { + val currentSnapshot = snapshot + if (currentSnapshot != null) { + PerfMonitorCard(snapshot = currentSnapshot, onClose = onClose) + } else { + PerfMonitorCard( + snapshot = PerfMonitorDataSource.Snapshot( + uiTrack = PerfMonitorDataSource.Track("UI", 0, emptyList()), + jsTrack = PerfMonitorDataSource.Track("JS", 0, emptyList()), + rssMB = 0.0, + layoutDurationMs = 0.0 + ), + onClose = onClose + ) + } + } + } + + container.addView( + composeView, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ) + ) + } + + fun setReactContext(context: ReactContext?) { + this.reactContext = context + if (isShowing && container.parent == null) { + show() + } + } + + fun show() { + if (isShowing) { + return + } + + mainHandler.post { + if (isShowing) { + return@post + } + + val activity = reactContext?.currentActivity + val rootView = + activity?.window?.decorView?.findViewById(android.R.id.content) + + if (rootView == null) { + isShowing = true + return@post + } + + isShowing = true + lifecycleOwner.resume() + dataSource.addListener(this) + + try { + val displayMetrics = context.resources.displayMetrics + val screenWidth = displayMetrics.widthPixels + val topMargin = (100 * displayMetrics.density).toInt() + + val params = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ) + + rootView.addView(container, params) + + container.post { + val x = (screenWidth - container.width) / 2f + container.x = x + container.y = topMargin.toFloat() + } + } catch (_: Throwable) { + isShowing = false + lifecycleOwner.pause() + dataSource.removeListener(this) + } + } + } + + fun hide() { + if (!isShowing) return + + mainHandler.post { + if (!isShowing) { + return@post + } + isShowing = false + dataSource.removeListener(this) + try { + (container.parent as? ViewGroup)?.removeView(container) + } catch (_: Throwable) { + } + lifecycleOwner.pause() + } + } + + fun isShowing() = isShowing + + override fun onSnapshot(snapshot: PerfMonitorDataSource.Snapshot) { + this.snapshot = snapshot + } +} + +@Composable +private fun PerfMonitorCard( + snapshot: PerfMonitorDataSource.Snapshot, + onClose: () -> Unit +) { + Surface( + modifier = Modifier + .fillMaxWidth(0.9f) + .border(1.dp, Color.White.copy(alpha = 0.08f), RoundedCornerShape(18.dp)), + shape = RoundedCornerShape(18.dp), + color = Color(0xFF1C1F29), + shadowElevation = 16.dp + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Header(onClose = onClose) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + UITrackCard(snapshot.uiTrack, Modifier.weight(1f)) + UITrackCard(snapshot.jsTrack, Modifier.weight(1f)) + } + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + StatTile( + title = "RAM", + value = "${snapshot.rssMB.format()} MB", + modifier = Modifier.weight(1f) + ) + StatTile( + title = "Hermes", + value = "โ€”", + modifier = Modifier.weight(1f) + ) + StatTile( + title = "Layout", + value = "${snapshot.layoutDurationMs.format(1)} ms", + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Composable +private fun Header(onClose: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Performance monitor", + color = Color.White, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f) + ) + Icon( + painter = painterResource(android.R.drawable.ic_menu_close_clear_cancel), + contentDescription = "Close", + tint = Color.White.copy(alpha = 0.8f), + modifier = Modifier + .clickable(onClick = onClose) + .padding(4.dp) + ) + } +} + +@Composable +private fun UITrackCard(track: PerfMonitorDataSource.Track, modifier: Modifier = Modifier) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .background(Color.White.copy(alpha = 0.08f), RoundedCornerShape(14.dp)) + .padding(12.dp) + ) { + Graph(track.history) + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Text( + text = track.label.uppercase(), + color = Color.White.copy(alpha = 0.65f), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.weight(1f) + ) + Text( + text = "${track.currentFps} fps", + color = Color.White, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold) + ) + } + } +} + +@Composable +private fun Graph(samples: List) { + val clamped = samples.map { min(max(it, 0.0), 120.0) } + val accentColor = Color(0xFF458CFA) + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(58.dp) + ) { + if (clamped.isEmpty()) return@Canvas + + val stepX = if (clamped.size <= 1) size.width else size.width / (clamped.size - 1) + val points = clamped.mapIndexed { index, value -> + val x = stepX * index + val normalized = (value / 120.0f).coerceIn(0.0, 1.0) + val y = size.height - (normalized * size.height) + Offset(x, y.toFloat()) + } + + if (points.isEmpty()) return@Canvas + + val gradientPath = Path().apply { + moveTo(points.first().x, size.height) + lineTo(points.first().x, points.first().y) + points.forEach { point -> + lineTo(point.x, point.y) + } + lineTo(points.last().x, size.height) + close() + } + + drawPath( + path = gradientPath, + brush = Brush.verticalGradient( + colors = listOf( + accentColor.copy(alpha = 0.5f), + accentColor.copy(alpha = 0.08f) + ), + startY = 0f, + endY = size.height + ), + style = Fill + ) + + for (i in 0 until points.size - 1) { + drawLine( + color = accentColor, + start = points[i], + end = points[i + 1], + strokeWidth = 2.2f + ) + } + } +} + +@Composable +private fun StatTile(title: String, value: String, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(Color.White.copy(alpha = 0.08f), RoundedCornerShape(14.dp)) + .padding(vertical = 10.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = title, + color = Color.White.copy(alpha = 0.6f), + style = MaterialTheme.typography.labelMedium + ) + Text( + text = value, + color = Color.White, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold) + ) + } +} + +private fun Double.format(decimals: Int = 2): String = String.format("%.${decimals}f", this) + +private val perfMonitorColorScheme = darkColorScheme( + primary = Color(0xFF458CFA), + onPrimary = Color.White, + secondary = Color(0xFF458CFA), + background = Color(0xFF1C1F29), + surface = Color(0xFF1C1F29), + onSurface = Color.White +) + +private class OverlayLifecycleOwner : LifecycleOwner, SavedStateRegistryOwner { + private val lifecycleRegistry = LifecycleRegistry(this) + private val savedStateRegistryController = SavedStateRegistryController.create(this) + + init { + lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED + savedStateRegistryController.performRestore(null) + } + + override val lifecycle: Lifecycle + get() = lifecycleRegistry + + override val savedStateRegistry: SavedStateRegistry + get() = savedStateRegistryController.savedStateRegistry + + fun resume() { + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + } + + fun pause() { + lifecycleRegistry.currentState = Lifecycle.State.CREATED + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/notifications/NotificationHelper.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/notifications/NotificationHelper.kt index c1290f2239f623..717072ee5254a6 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/notifications/NotificationHelper.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/notifications/NotificationHelper.kt @@ -53,8 +53,7 @@ object NotificationHelper { manifest: Manifest, exponentManifest: ExponentManifest ): Int { - val colorStringLocal = colorString ?: manifest.getNotificationPreferences()?.getNullable(ExponentManifest.MANIFEST_NOTIFICATION_COLOR_KEY) - return if (colorString != null && ColorParser.isValid(colorStringLocal)) { + return if (colorString != null && ColorParser.isValid(colorString)) { Color.parseColor(colorString) } else { exponentManifest.getColorFromManifest(manifest) @@ -67,17 +66,7 @@ object NotificationHelper { exponentManifest: ExponentManifest, bitmapListener: BitmapListener? ) { - val notificationPreferences = manifest.getNotificationPreferences() - var iconUrl: String? - if (url == null) { - iconUrl = manifest.getIconUrl() - if (notificationPreferences != null) { - iconUrl = notificationPreferences.getNullable(ExponentManifest.MANIFEST_NOTIFICATION_ICON_URL_KEY) - } - } else { - iconUrl = url - } - + val iconUrl = url ?: manifest.getIconUrl() exponentManifest.loadIconBitmap(iconUrl, bitmapListener!!) } diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/ApolloClientService.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/ApolloClientService.kt index 501dbde21a4af6..f6ad709deb32ab 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/ApolloClientService.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/ApolloClientService.kt @@ -17,10 +17,14 @@ import host.exp.exponent.graphql.Home_ViewerPrimaryAccountNameQuery import host.exp.exponent.graphql.ProjectsQuery import host.exp.exponent.graphql.fragment.CurrentUserActorData import host.exp.exponent.graphql.type.AppPlatform +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.map import okhttp3.OkHttpClient class ApolloClientService( - httpClient: OkHttpClient + httpClient: OkHttpClient, + sessionRepository: SessionRepository ) { private val normalizedCacheFactory = MemoryCacheFactory(maxSizeBytes = 10 * 1024 * 1024) @@ -29,24 +33,22 @@ class ApolloClientService( .serverUrl("https://exp.host/--/graphql") .okHttpClient(httpClient) .normalizedCache(normalizedCacheFactory) - .addHttpInterceptor(AuthInterceptor { null }) + .addHttpInterceptor(AuthInterceptor(sessionRepository)) .fetchPolicy(FetchPolicy.CacheAndNetwork) .build() - suspend fun currentUser(): CurrentUserActorData? { - return apolloClient.query( - Home_CurrentUserActorQuery() - ) - .execute() - .dataOrThrow() - .meUserActor - ?.currentUserActorData + fun currentUser(): Flow { + return apolloClient.query(Home_CurrentUserActorQuery()) + .toFlow() + .map { response -> + response.data?.meUserActor?.currentUserActorData + } } - suspend fun branchDetails( + fun branchDetails( name: String, appId: String - ): BranchDetailsQuery.ById { + ): Flow { return apolloClient.query( BranchDetailsQuery( name = name, @@ -54,10 +56,9 @@ class ApolloClientService( platform = AppPlatform.ANDROID ) ) - .execute() - .dataOrThrow() - .app - .byId + .toFlow().map { + it.data?.app?.byId + } } fun branches( @@ -73,7 +74,7 @@ class ApolloClientService( offset = offset ) ) - .execute() + .toFlow().last() .dataOrThrow() .app .byId @@ -82,6 +83,23 @@ class ApolloClientService( ) } + fun branches( + appId: String, + count: Int + ): Flow> { + return apolloClient.query( + BranchesForProjectQuery( + appId = appId, + platform = AppPlatform.ANDROID, + limit = count, + offset = 0 + ) + ) + .toFlow().map { + it.data?.app?.byId?.updateBranches ?: emptyList() + } + } + fun apps( accountName: String ): Paginator { @@ -95,7 +113,7 @@ class ApolloClientService( offset = offset ) ) - .execute() + .toFlow().last() .dataOrThrow() .account .byName @@ -104,19 +122,30 @@ class ApolloClientService( ) } - suspend fun app( - appId: String - ): ProjectsQuery.ById { + fun apps(accountName: String, count: Int = 10): Flow> { + return apolloClient.query( + Home_AccountAppsQuery( + accountName = accountName, + platform = AppPlatform.ANDROID, + limit = count, + offset = 0 + ) + ).toFlow() + .map { response -> + response.data?.account?.byName?.apps ?: emptyList() + } + } + + fun app(appId: String): Flow { return apolloClient.query( ProjectsQuery( appId = appId, platform = AppPlatform.ANDROID ) - ) - .execute() - .dataOrThrow() - .app - .byId + ).toFlow() + .map { response -> + response.data?.app?.byId + } } fun snacks( @@ -131,15 +160,24 @@ class ApolloClientService( offset = offset ) ) - .execute() - .dataOrThrow() - .account - .byName - .snacks + .toFlow().last().dataOrThrow().account.byName.snacks } ) } + fun snacks(accountName: String, count: Int = 10): Flow> { + return apolloClient.query( + Home_AccountSnacksQuery( + accountName = accountName, + limit = count, + offset = 0 + ) + ).toFlow() + .map { response -> + response.data?.account?.byName?.snacks ?: emptyList() + } + } + suspend fun primaryAccount(): Home_ViewerPrimaryAccountNameQuery.PrimaryAccount? { return apolloClient.query( Home_ViewerPrimaryAccountNameQuery() diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/ExponentHistoryService.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/ExponentHistoryService.kt new file mode 100644 index 00000000000000..91ce31ea847147 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/ExponentHistoryService.kt @@ -0,0 +1,90 @@ +package host.exp.exponent.services + +import android.util.Log +import com.google.gson.FormattingStyle +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import expo.modules.manifests.core.Manifest +import host.exp.exponent.analytics.EXL +import host.exp.exponent.storage.ExponentSharedPreferences +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.Date +import javax.inject.Singleton + +data class HistoryItem( + val manifestUrl: String, + val name: String, + val iconUrl: String?, + val timestamp: Long = Date().time +) { + constructor(manifestUrl: String, manifest: Manifest) : this( + manifestUrl = manifestUrl, + name = manifest.getName() ?: manifestUrl, + iconUrl = manifest.getIconUrl(), + timestamp = Date().time + ) +} + +@Singleton +class ExponentHistoryService( + val exponentSharedPreferences: ExponentSharedPreferences +) { + private val gson = GsonBuilder() + .setFormattingStyle(FormattingStyle.COMPACT) + .create() + + private val _history = MutableStateFlow(restoreHistory()) + val history = _history.asStateFlow() + + fun getLastCrashDate(): Long { + return exponentSharedPreferences.getLong(ExponentSharedPreferences.ExponentSharedPreferencesKey.LAST_FATAL_ERROR_DATE_KEY) + } + + fun addHistoryItem(item: HistoryItem) { + val currentHistory = _history.value.toMutableList() + currentHistory.removeAll { it.manifestUrl == item.manifestUrl } + currentHistory.add(0, item) + _history.value = currentHistory + saveHistory(currentHistory) + } + + private fun saveHistory(history: List) { + runCatching { + val jsonString = gson.toJson(history) + exponentSharedPreferences.setString( + ExponentSharedPreferences.ExponentSharedPreferencesKey.HISTORY, + jsonString + ) + }.onFailure { + EXL.e(TAG, "Error saving history to SharedPreferences with: ${it.message}") + } + } + + private fun restoreHistory(): List { + val savedHistory = exponentSharedPreferences.getString( + ExponentSharedPreferences.ExponentSharedPreferencesKey.HISTORY + ) ?: return emptyList() + + return runCatching { + gson.fromJson( + savedHistory, + object : TypeToken>() {} + ) + }.onFailure { + Log.e(TAG, "Error restoring history from SharedPreferences with: ${it.message}") + exponentSharedPreferences.delete( + ExponentSharedPreferences.ExponentSharedPreferencesKey.HISTORY + ) + }.getOrNull() ?: emptyList() + } + + fun clearHistory() { + _history.value = emptyList() + exponentSharedPreferences.delete(ExponentSharedPreferences.ExponentSharedPreferencesKey.HISTORY) + } + + companion object { + private val TAG = ExponentHistoryService::class.java.simpleName + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/RESTApiClient.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/RESTApiClient.kt new file mode 100644 index 00000000000000..493f3cacdd4d14 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/RESTApiClient.kt @@ -0,0 +1,83 @@ +package host.exp.exponent.services + +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import kotlin.reflect.KType +import kotlin.reflect.javaType + +const val apiV2BaseUrl = "https://exp.host/--/api/v2/" + +class RESTApiClient(private val sessionRepository: SessionRepository) { + private val client = OkHttpClient() + private val gson = Gson() + + @OptIn(ExperimentalStdlibApi::class) + suspend fun sendAuthenticatedApiV2Request(route: String, type: KType): T { + val sessionSecret = sessionRepository.getSessionSecret() + ?: throw IllegalStateException("Must be logged in to perform request") + + val url = apiV2BaseUrl + route + + val request = Request.Builder() + .url(url) + // TODO: Re-add SDK version header + .addHeader("Expo-SDK-Version", "54") + .addHeader("Expo-Platform", "android") + .addHeader("Expo-Session", sessionSecret) + .get() + .build() + + // Execute the request on a background thread + return withContext(Dispatchers.IO) { + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("Unexpected code ${response.code} from ${response.request.url}") + } + + val responseBody = response.body?.string() + ?: throw IOException("Empty response body from ${response.request.url}") + + // Deserialize the JSON response into the specified type + gson.fromJson(responseBody, type.javaType) + } + } + } + + @OptIn(ExperimentalStdlibApi::class) + suspend fun sendUnauthenticatedApiV2Request(route: String, type: KType, body: B? = null): T { + val url = apiV2BaseUrl + route + + val requestBuilder = Request.Builder().url(url) + + if (body != null) { + val jsonBody = gson.toJson(body) + requestBuilder.post(jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())) + } else { + requestBuilder.get() + } + + val request = requestBuilder.build() + + // Execute the request on a background thread + return withContext(Dispatchers.IO) { + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + val errorBody = response.body?.string() + throw IOException("Unexpected code ${response.code} from ${response.request.url}. Body: $errorBody") + } + + val responseBody = response.body?.string() + ?: throw IOException("Empty response body from ${response.request.url}") + + // Deserialize the JSON response into the specified type + gson.fromJson(responseBody, type.javaType) + } + } + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/SessionRepository.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/SessionRepository.kt new file mode 100644 index 00000000000000..efe9f5f62217a7 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/services/SessionRepository.kt @@ -0,0 +1,71 @@ +package host.exp.exponent.services + +import android.content.Context +import androidx.core.content.edit + +enum class ThemeSetting { + Automatic, + Light, + Dark +} + +class SessionRepository(context: Context) { + private val sharedPreferences = context.getSharedPreferences( + "expo_session", + Context.MODE_PRIVATE + ) + + companion object { + private const val SESSION_SECRET_KEY = "session_secret" + private const val SELECTED_ACCOUNT_ID_KEY = "selected_account_id" + private const val RECENTS_KEY = "recents_history" + private const val THEME_KEY = "theme" + } + + fun saveThemeSetting(themeSetting: ThemeSetting) { + sharedPreferences.edit(commit = true) { + putString(THEME_KEY, themeSetting.name) + } + } + + fun getThemeSetting(): ThemeSetting { + val themeName = sharedPreferences.getString(THEME_KEY, ThemeSetting.Automatic.name) + return try { + ThemeSetting.valueOf(themeName ?: ThemeSetting.Automatic.name) + } catch (_: IllegalArgumentException) { + ThemeSetting.Automatic + } + } + + fun saveSessionSecret(secret: String?) { + sharedPreferences.edit(commit = true) { + putString(SESSION_SECRET_KEY, secret) + } + } + + fun getSessionSecret(): String? { + return sharedPreferences.getString(SESSION_SECRET_KEY, null) + } + + fun clearSessionSecret() { + sharedPreferences.edit(commit = true) { + remove(SESSION_SECRET_KEY) + } + } + + fun saveSelectedAccountId(accountId: String?) { + sharedPreferences.edit(commit = true) { + putString(SELECTED_ACCOUNT_ID_KEY, accountId) + } + } + + fun clearSelectedAccountId() { + sharedPreferences.edit(commit = true) { + remove(SELECTED_ACCOUNT_ID_KEY) + } + } + + fun getSelectedAccountId(): String? { + return sharedPreferences.getString(SELECTED_ACCOUNT_ID_KEY, null) + } +} diff --git a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.kt b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.kt index f7c4d18d887b1f..8b80115d109ff8 100644 --- a/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.kt +++ b/apps/expo-go/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.kt @@ -2,6 +2,7 @@ package host.exp.exponent.storage import android.content.Context +import androidx.core.content.edit import host.exp.exponent.analytics.EXL import host.exp.exponent.kernel.ExperienceKey import host.exp.expoview.ExpoViewBuildConfig @@ -26,7 +27,7 @@ class ExponentSharedPreferences(val context: Context) { sharedPreferences.getBoolean(key.preferenceKey, defaultValue) fun setBoolean(key: ExponentSharedPreferencesKey, value: Boolean) = - sharedPreferences.edit().putBoolean(key.preferenceKey, value).apply() + sharedPreferences.edit(commit = true) { putBoolean(key.preferenceKey, value) } fun getInteger(key: ExponentSharedPreferencesKey) = getInteger(key, 0) @@ -34,13 +35,13 @@ class ExponentSharedPreferences(val context: Context) { sharedPreferences.getInt(key.preferenceKey, defaultValue) fun setInteger(key: ExponentSharedPreferencesKey, value: Int) = - sharedPreferences.edit().putInt(key.preferenceKey, value).apply() + sharedPreferences.edit(commit = true) { putInt(key.preferenceKey, value) } fun getLong(key: ExponentSharedPreferencesKey) = sharedPreferences.getLong(key.preferenceKey, 0) fun setLong(key: ExponentSharedPreferencesKey, value: Long) = - sharedPreferences.edit().putLong(key.preferenceKey, value).apply() + sharedPreferences.edit(commit = true) { putLong(key.preferenceKey, value) } fun getString(key: ExponentSharedPreferencesKey) = getString(key, null) @@ -49,10 +50,10 @@ class ExponentSharedPreferences(val context: Context) { sharedPreferences.getString(key.preferenceKey, defaultValue) fun setString(key: ExponentSharedPreferencesKey, value: String?) = - sharedPreferences.edit().putString(key.preferenceKey, value).apply() + sharedPreferences.edit(commit = true) { putString(key.preferenceKey, value) } fun delete(key: ExponentSharedPreferencesKey) = - sharedPreferences.edit().remove(key.preferenceKey).apply() + sharedPreferences.edit(commit = true) { remove(key.preferenceKey) } fun shouldUseEmbeddedKernel() = getBoolean(ExponentSharedPreferencesKey.USE_EMBEDDED_KERNEL_KEY) @@ -80,12 +81,12 @@ class ExponentSharedPreferences(val context: Context) { } fun removeLegacyManifest(manifestUrl: String) = - sharedPreferences.edit().remove(manifestUrl).apply() + sharedPreferences.edit(commit = true) { remove(manifestUrl) } fun updateExperienceMetadata(experienceKey: ExperienceKey, metadata: JSONObject) = - sharedPreferences.edit() - .putString(EXPERIENCE_METADATA_PREFIX + experienceKey.scopeKey, metadata.toString()) - .apply() + sharedPreferences.edit(commit = true) { + putString(EXPERIENCE_METADATA_PREFIX + experienceKey.scopeKey, metadata.toString()) + } fun getExperienceMetadata(experienceKey: ExperienceKey): JSONObject? { val jsonString = @@ -145,6 +146,7 @@ class ExponentSharedPreferences(val context: Context) { SHOULD_NOT_USE_KERNEL_CACHE("should_not_use_kernel_cache"), KERNEL_REVISION_ID("kernel_revision_id"), EXPO_AUTH_SESSION("expo_auth_session"), - OKHTTP_CACHE_VERSION_KEY("okhttp_cache_version") + OKHTTP_CACHE_VERSION_KEY("okhttp_cache_version"), + HISTORY("history") } } diff --git a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExperiencePackagePicker.kt b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExperiencePackagePicker.kt index 0f7d10303dcd9a..acb02972facde6 100644 --- a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExperiencePackagePicker.kt +++ b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExperiencePackagePicker.kt @@ -3,12 +3,10 @@ package versioned.host.exp.exponent import expo.modules.application.ApplicationModule import expo.modules.asset.AssetModule import expo.modules.audio.AudioModule -import expo.modules.av.AVModule -import expo.modules.av.AVPackage -import expo.modules.av.video.VideoViewModule import expo.modules.backgroundfetch.BackgroundFetchModule import expo.modules.backgroundtask.BackgroundTaskModule import expo.modules.battery.BatteryModule +import expo.modules.blob.BlobModule import expo.modules.blur.BlurModule import expo.modules.brightness.BrightnessModule import expo.modules.calendar.CalendarModule @@ -16,10 +14,11 @@ import expo.modules.camera.CameraViewModule import expo.modules.cellular.CellularModule import expo.modules.clipboard.ClipboardModule import expo.modules.constants.ConstantsModule -import expo.modules.constants.ConstantsPackage +import expo.modules.constants.ConstantsService import expo.modules.contacts.ContactsModule import expo.modules.core.interfaces.Package import expo.modules.crypto.CryptoModule +import expo.modules.crypto.aes.AesCryptoModule import expo.modules.device.DeviceModule import expo.modules.documentpicker.DocumentPickerModule import expo.modules.easclient.EASClientModule @@ -31,13 +30,14 @@ import expo.modules.font.FontUtilsModule import expo.modules.gl.GLModule import expo.modules.haptics.HapticsModule import expo.modules.image.ExpoImageModule -import expo.modules.imageloader.ImageLoaderPackage +import expo.modules.imageloader.ImageLoaderService import expo.modules.imagemanipulator.ImageManipulatorModule import expo.modules.imagepicker.ImagePickerModule import expo.modules.intentlauncher.IntentLauncherModule import expo.modules.keepawake.KeepAwakeModule import expo.modules.kotlin.ModulesProvider import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.services.Service import expo.modules.lineargradient.LinearGradientModule import expo.modules.linking.ExpoLinkingModule import expo.modules.linking.ExpoLinkingPackage @@ -71,8 +71,6 @@ import expo.modules.sensors.modules.PedometerModule import expo.modules.sharing.SharingModule import expo.modules.sms.SMSModule import expo.modules.speech.SpeechModule -import host.exp.exponent.experience.splashscreen.legacy.SplashScreenModule -import host.exp.exponent.experience.splashscreen.legacy.SplashScreenPackage import expo.modules.sqlite.SQLiteModule import expo.modules.storereview.StoreReviewModule import expo.modules.systemui.SystemUIModule @@ -84,13 +82,12 @@ import expo.modules.updates.UpdatesPackage import expo.modules.video.VideoModule import expo.modules.videothumbnails.VideoThumbnailsModule import expo.modules.webbrowser.WebBrowserModule +import host.exp.exponent.experience.splashscreen.legacy.SplashScreenModule +import host.exp.exponent.experience.splashscreen.legacy.SplashScreenPackage object ExperiencePackagePicker : ModulesProvider { private val EXPO_MODULES_PACKAGES = listOf( - AVPackage(), - ConstantsPackage(), ExpoLinkingPackage(), - ImageLoaderPackage(), NavigationBarPackage(), NotificationsPackage(), SplashScreenPackage(), @@ -116,7 +113,6 @@ object ExperiencePackagePicker : ModulesProvider { override fun getModulesMap(): Map, String?> = mapOf( AudioModule::class.java to null, - AVModule::class.java to null, ApplicationModule::class.java to null, // Sensors AccelerometerModule::class.java to null, @@ -137,9 +133,11 @@ object ExperiencePackagePicker : ModulesProvider { NotificationChannelGroupManagerModule::class.java to null, ExpoBackgroundNotificationTasksModule::class.java to null, // End of Notifications + AesCryptoModule::class.java to null, BatteryModule::class.java to null, BackgroundFetchModule::class.java to null, BackgroundTaskModule::class.java to null, + BlobModule::class.java to null, BlurModule::class.java to null, CalendarModule::class.java to null, CameraViewModule::class.java to null, @@ -188,8 +186,14 @@ object ExperiencePackagePicker : ModulesProvider { TrackingTransparencyModule::class.java to null, VideoThumbnailsModule::class.java to null, VideoModule::class.java to null, - VideoViewModule::class.java to null, WebBrowserModule::class.java to null, BrightnessModule::class.java to null ) + + override fun getServices(): List> { + return listOf( + ConstantsService::class.java, + ImageLoaderService::class.java + ) + } } diff --git a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.kt b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.kt index aa22b50b7e2f2a..9bd931ff5644e8 100644 --- a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.kt +++ b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/ExponentPackage.kt @@ -62,14 +62,12 @@ class ExponentPackage : ReactPackage { isKernel: Boolean, experienceProperties: Map, manifest: Manifest, - expoPackages: List, - moduleProvider: ModulesProvider, singletonModules: List? ) { this.isKernel = isKernel this.experienceProperties = experienceProperties this.manifest = manifest - moduleRegistryAdapter = createDefaultModuleRegistryAdapterForPackages(expoPackages, singletonModules, moduleProvider) + moduleRegistryAdapter = createDefaultModuleRegistryAdapterForPackages(emptyList(), singletonModules, null) } constructor( @@ -131,10 +129,10 @@ class ExponentPackage : ReactPackage { nativeModules.add(NetInfoModule(reactContext)) nativeModules.add(EdgeToEdgeModule(reactContext)) nativeModules.add(KeyboardControllerModule(reactContext)) - nativeModules.addAll(SvgPackage().getReactModuleInfoProvider().getReactModuleInfos().map { SvgPackage().getModule(it.value.name, reactContext)!! }) - nativeModules.addAll(MapsPackage().createNativeModules(reactContext)) - nativeModules.addAll(RNDateTimePickerPackage().getReactModuleInfoProvider().getReactModuleInfos().map { RNDateTimePickerPackage().getModule(it.value.name, reactContext)!! }) - nativeModules.addAll(stripePackage.getReactModuleInfoProvider().getReactModuleInfos().map { stripePackage.getModule(it.value.name, reactContext)!! }) + nativeModules.addAll(svgPackage.getReactModuleInfoProvider().getReactModuleInfos().values.mapNotNull { svgPackage.getModule(it.name, reactContext) }) + nativeModules.addAll(mapsPackage.getReactModuleInfoProvider().getReactModuleInfos().values.mapNotNull { mapsPackage.getModule(it.name, reactContext) }) + nativeModules.addAll(dateTimePackage.getReactModuleInfoProvider().getReactModuleInfos().values.mapNotNull { dateTimePackage.getModule(it.name, reactContext) }) + nativeModules.addAll(stripePackage.getReactModuleInfoProvider().getReactModuleInfos().values.mapNotNull { stripePackage.getModule(it.name, reactContext) }) nativeModules.addAll(skiaPackage.createNativeModules(reactContext)) // Call to create native modules has to be at the bottom -- @@ -214,12 +212,13 @@ class ExponentPackage : ReactPackage { // Need to avoid initializing duplicated packages private val stripePackage = StripeSdkPackage() private val skiaPackage = RNSkiaPackage() + private val mapsPackage = MapsPackage() + private val dateTimePackage = RNDateTimePickerPackage() + private val svgPackage = SvgPackage() fun kernelExponentPackage( context: Context, manifest: Manifest, - expoPackages: List, - modulesProvider: ModulesProvider, initialURL: String? ): ExponentPackage { val kernelExperienceProperties = mutableMapOf( @@ -230,13 +229,11 @@ class ExponentPackage : ReactPackage { this[KernelConstants.INTENT_URI_KEY] = initialURL } } - val singletonModules = getOrCreateSingletonModules(context, manifest, expoPackages) + val singletonModules = getOrCreateSingletonModules(context, manifest, null) return ExponentPackage( true, kernelExperienceProperties, manifest, - expoPackages, - modulesProvider, singletonModules ) } diff --git a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ConstantsBinding.kt b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ConstantsBinding.kt index 2231e41347672d..1fbb0a11d61267 100644 --- a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ConstantsBinding.kt +++ b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ConstantsBinding.kt @@ -2,52 +2,56 @@ package versioned.host.exp.exponent.modules.universal import android.content.Context -import javax.inject.Inject -import host.exp.exponent.storage.ExponentSharedPreferences -import host.exp.exponent.kernel.ExpoViewKernel import expo.modules.constants.ConstantsService -import expo.modules.interfaces.constants.ConstantsInterface import expo.modules.manifests.core.Manifest import host.exp.exponent.Constants import host.exp.exponent.di.NativeModuleDepsProvider +import host.exp.exponent.kernel.ExpoViewKernel +import host.exp.exponent.storage.ExponentSharedPreferences import org.json.JSONException +import javax.inject.Inject class ConstantsBinding( context: Context, private val experienceProperties: Map, private val manifest: Manifest -) : ConstantsService(context), ConstantsInterface { +) : ConstantsService(context) { @Inject lateinit var exponentSharedPreferences: ExponentSharedPreferences - override fun getConstants(): Map { - return super.getConstants().toMutableMap().apply { - this["expoVersion"] = ExpoViewKernel.instance.versionName - this["manifest"] = manifest.toString() - this["nativeAppVersion"] = ExpoViewKernel.instance.versionName - this["nativeBuildVersion"] = Constants.ANDROID_VERSION_CODE - this["supportedExpoSdks"] = listOf(Constants.SDK_VERSION) - this["appOwnership"] = "expo" - this["executionEnvironment"] = executionEnvironment.string + override val constants: Map + get() { + return super + .constants + .toMutableMap() + .apply { + this["expoVersion"] = ExpoViewKernel.instance.versionName + this["manifest"] = manifest.toString() + this["nativeAppVersion"] = ExpoViewKernel.instance.versionName + this["nativeBuildVersion"] = Constants.ANDROID_VERSION_CODE + this["supportedExpoSdks"] = listOf(Constants.SDK_VERSION) + this["appOwnership"] = "expo" + this["executionEnvironment"] = executionEnvironment.string - this.putAll(experienceProperties) + this.putAll(experienceProperties) - this["platform"] = mapOf( - "android" to mapOf( - "versionCode" to null - ) - ) - this["isDetached"] = false + this["platform"] = mapOf( + "android" to mapOf( + "versionCode" to null + ) + ) + this["isDetached"] = false + } } - } - override fun getAppScopeKey(): String? { - return try { - manifest.getScopeKey() - } catch (e: JSONException) { - null + override val appScopeKey: String? + get() { + return try { + manifest.getScopeKey() + } catch (_: JSONException) { + null + } } - } private val executionEnvironment: ExecutionEnvironment get() = ExecutionEnvironment.STORE_CLIENT diff --git a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ExpoModuleRegistryAdapter.kt b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ExpoModuleRegistryAdapter.kt index 9a005e8605a052..615d3764c78cd2 100644 --- a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ExpoModuleRegistryAdapter.kt +++ b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/ExpoModuleRegistryAdapter.kt @@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReactApplicationContext import expo.modules.adapters.react.ModuleRegistryAdapter import expo.modules.adapters.react.ReactModuleRegistryProvider import expo.modules.core.interfaces.RegistryLifecycleListener +import expo.modules.interfaces.constants.ConstantsInterface import expo.modules.kotlin.ModulesProvider import expo.modules.kotlin.services.AppDirectoriesService import expo.modules.kotlin.services.FilePermissionService @@ -15,7 +16,6 @@ import versioned.host.exp.exponent.core.modules.ExpoGoModule import versioned.host.exp.exponent.core.modules.ExpoGoUpdatesModule import versioned.host.exp.exponent.modules.api.notifications.ScopedNotificationsCategoriesSerializer import versioned.host.exp.exponent.modules.api.notifications.channels.ScopedNotificationsChannelsProvider -import versioned.host.exp.exponent.modules.universal.av.SharedCookiesDataSourceFactoryProvider import versioned.host.exp.exponent.modules.universal.notifications.ScopedExpoNotificationCategoriesModule import versioned.host.exp.exponent.modules.universal.notifications.ScopedExpoNotificationPresentationModule import versioned.host.exp.exponent.modules.universal.notifications.ScopedNotificationScheduler @@ -38,17 +38,6 @@ open class ExpoModuleRegistryAdapter( ): List { val moduleRegistry = mModuleRegistryProvider[scopedContext] - moduleRegistry.registerInternalModule(SharedCookiesDataSourceFactoryProvider()) - - // Overriding expo-constants/ConstantsService -- binding provides manifest and other expo-related constants - moduleRegistry.registerInternalModule( - ConstantsBinding( - scopedContext, - experienceProperties, - manifest - ) - ) - // Overriding expo-notifications classes moduleRegistry.registerInternalModule( ScopedNotificationsChannelsProvider( @@ -81,6 +70,14 @@ open class ExpoModuleRegistryAdapter( with(appContext.services) { register(FilePermissionService::class.java, ScopedFilePermissionService(scopedContext)) register(AppDirectoriesService::class.java, AppDirectoriesService(scopedContext)) + register( + ConstantsInterface::class.java, + ConstantsBinding( + scopedContext, + experienceProperties, + manifest + ) + ) } with(appContext.registry) { diff --git a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/av/SharedCookiesDataSourceFactory.kt b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/av/SharedCookiesDataSourceFactory.kt deleted file mode 100644 index 759913e226d43d..00000000000000 --- a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/av/SharedCookiesDataSourceFactory.kt +++ /dev/null @@ -1,29 +0,0 @@ -package versioned.host.exp.exponent.modules.universal.av - -import com.facebook.react.bridge.ReactContext -import com.facebook.react.modules.network.NetworkingModule -import com.google.android.exoplayer2.upstream.DataSource -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory -import com.google.android.exoplayer2.upstream.TransferListener -import expo.modules.av.player.datasource.CustomHeadersOkHttpDataSourceFactory - -class SharedCookiesDataSourceFactory( - reactApplicationContext: ReactContext, - userAgent: String, - requestHeaders: Map?, - transferListener: TransferListener? -) : DataSource.Factory { - private val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory( - reactApplicationContext, - transferListener, - CustomHeadersOkHttpDataSourceFactory( - (reactApplicationContext.catalystInstance.getNativeModule("Networking") as NetworkingModule?)!!.client, - userAgent, - requestHeaders - ) - ) - - override fun createDataSource(): DataSource { - return dataSourceFactory.createDataSource() - } -} diff --git a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/av/SharedCookiesDataSourceFactoryProvider.kt b/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/av/SharedCookiesDataSourceFactoryProvider.kt deleted file mode 100644 index a4523c53d1d984..00000000000000 --- a/apps/expo-go/android/expoview/src/main/java/versioned/host/exp/exponent/modules/universal/av/SharedCookiesDataSourceFactoryProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package versioned.host.exp.exponent.modules.universal.av - -import android.content.Context -import com.facebook.react.bridge.ReactContext -import com.google.android.exoplayer2.upstream.DataSource -import com.google.android.exoplayer2.upstream.TransferListener -import expo.modules.av.player.datasource.SharedCookiesDataSourceFactoryProvider -import expo.modules.core.ModuleRegistry -import host.exp.exponent.utils.ScopedContext - -class SharedCookiesDataSourceFactoryProvider : SharedCookiesDataSourceFactoryProvider() { - override fun createFactory( - context: Context, - moduleRegistry: ModuleRegistry, - userAgent: String, - requestHeaders: Map?, - transferListener: TransferListener - ): DataSource.Factory { - val reactContext: ReactContext = when (context) { - is ReactContext -> context - is ScopedContext -> context.context as ReactContext - else -> throw Exception("Invalid context supplied to SharedCookiesDataSourceFactoryProvider") - } - return SharedCookiesDataSourceFactory(reactContext, userAgent, requestHeaders, transferListener) - } -} diff --git a/apps/expo-go/android/expoview/src/main/res/drawable-mdpi/fab.png b/apps/expo-go/android/expoview/src/main/res/drawable-mdpi/fab.png new file mode 100644 index 00000000000000..05621445ec4bfc Binary files /dev/null and b/apps/expo-go/android/expoview/src/main/res/drawable-mdpi/fab.png differ diff --git a/apps/expo-go/android/expoview/src/main/res/drawable-xhdpi/fab.png b/apps/expo-go/android/expoview/src/main/res/drawable-xhdpi/fab.png new file mode 100644 index 00000000000000..7add20ca709d6b Binary files /dev/null and b/apps/expo-go/android/expoview/src/main/res/drawable-xhdpi/fab.png differ diff --git a/apps/expo-go/android/expoview/src/main/res/drawable-xxhdpi/fab.png b/apps/expo-go/android/expoview/src/main/res/drawable-xxhdpi/fab.png new file mode 100644 index 00000000000000..b5e36a5610748a Binary files /dev/null and b/apps/expo-go/android/expoview/src/main/res/drawable-xxhdpi/fab.png differ diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/account_circle.xml b/apps/expo-go/android/expoview/src/main/res/drawable/account_circle.xml new file mode 100644 index 00000000000000..f2a9b67d401f5e --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/account_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/arrow_back.xml b/apps/expo-go/android/expoview/src/main/res/drawable/arrow_back.xml new file mode 100644 index 00000000000000..0e2e8635a3912a --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,11 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/check.xml b/apps/expo-go/android/expoview/src/main/res/drawable/check.xml new file mode 100644 index 00000000000000..280f0bd8046d84 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/check.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/chevron_right.xml b/apps/expo-go/android/expoview/src/main/res/drawable/chevron_right.xml new file mode 100644 index 00000000000000..16df660e2e507f --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/chevron_right.xml @@ -0,0 +1,11 @@ + + + diff --git a/apps/expo-go/src/assets/cli.png b/apps/expo-go/android/expoview/src/main/res/drawable/cli.png similarity index 100% rename from apps/expo-go/src/assets/cli.png rename to apps/expo-go/android/expoview/src/main/res/drawable/cli.png diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/close.xml b/apps/expo-go/android/expoview/src/main/res/drawable/close.xml new file mode 100644 index 00000000000000..4153160ecead20 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/close.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/home.xml b/apps/expo-go/android/expoview/src/main/res/drawable/home.xml new file mode 100644 index 00000000000000..edc0cadcfafc98 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/home.xml @@ -0,0 +1,13 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/launch_at_start.xml b/apps/expo-go/android/expoview/src/main/res/drawable/launch_at_start.xml new file mode 100644 index 00000000000000..d4f3adb9aee1d9 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/launch_at_start.xml @@ -0,0 +1,12 @@ + + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/qr_code.xml b/apps/expo-go/android/expoview/src/main/res/drawable/qr_code.xml new file mode 100644 index 00000000000000..382fc559e29ab0 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/qr_code.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/settings.xml b/apps/expo-go/android/expoview/src/main/res/drawable/settings.xml new file mode 100644 index 00000000000000..f20bbddd03e0b6 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/shake.xml b/apps/expo-go/android/expoview/src/main/res/drawable/shake.xml new file mode 100644 index 00000000000000..bec65d83254659 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/shake.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/src/assets/snack.png b/apps/expo-go/android/expoview/src/main/res/drawable/snack.png similarity index 100% rename from apps/expo-go/src/assets/snack.png rename to apps/expo-go/android/expoview/src/main/res/drawable/snack.png diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/terminal_icon.png b/apps/expo-go/android/expoview/src/main/res/drawable/terminal_icon.png new file mode 100644 index 00000000000000..da51537d2444e3 Binary files /dev/null and b/apps/expo-go/android/expoview/src/main/res/drawable/terminal_icon.png differ diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/theme_auto.xml b/apps/expo-go/android/expoview/src/main/res/drawable/theme_auto.xml new file mode 100644 index 00000000000000..1c9bc5500e55f6 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/theme_auto.xml @@ -0,0 +1,15 @@ + + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/theme_dark.xml b/apps/expo-go/android/expoview/src/main/res/drawable/theme_dark.xml new file mode 100644 index 00000000000000..793a4e96c40d1d --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/theme_dark.xml @@ -0,0 +1,9 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/theme_light.xml b/apps/expo-go/android/expoview/src/main/res/drawable/theme_light.xml new file mode 100644 index 00000000000000..6f5c418c867444 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/theme_light.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/three_finger_long_press.xml b/apps/expo-go/android/expoview/src/main/res/drawable/three_finger_long_press.xml new file mode 100644 index 00000000000000..ed3a6f71a68f4f --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/three_finger_long_press.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/drawable/warning.xml b/apps/expo-go/android/expoview/src/main/res/drawable/warning.xml new file mode 100644 index 00000000000000..618f0e0ec09d00 --- /dev/null +++ b/apps/expo-go/android/expoview/src/main/res/drawable/warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/expo-go/android/expoview/src/main/res/values/strings.xml b/apps/expo-go/android/expoview/src/main/res/values/strings.xml index 4703e27b606514..a0dd88bd27da1f 100644 --- a/apps/expo-go/android/expoview/src/main/res/values/strings.xml +++ b/apps/expo-go/android/expoview/src/main/res/values/strings.xml @@ -54,11 +54,11 @@ Performance Monitor Unavailable contain false - + Make sure you are signed in to the same Expo account on your computer and this app. Also verify that your computer is connected to the internet, and ideally to the same Wi-Fi network as your mobile device. Lastly, ensure that you are using the latest version of Expo CLI. Pull to refresh to update. - Currency (optional) - Unspecified - USD + Currency (optional) + Unspecified + USD diff --git a/apps/expo-go/android/gradle.properties b/apps/expo-go/android/gradle.properties index 95b61fdab577da..526171826d8498 100644 --- a/apps/expo-go/android/gradle.properties +++ b/apps/expo-go/android/gradle.properties @@ -42,3 +42,6 @@ edgeToEdgeEnabled=true kotlin.jvm.target.validation.mode=warning expo.devmenu.configureInRelease=true + +org.gradle.configuration-cache=true +org.gradle.configuration-cache.problems=warn diff --git a/apps/expo-go/android/settings.gradle b/apps/expo-go/android/settings.gradle index 7ec432689d24ba..9d2a00012493c7 100644 --- a/apps/expo-go/android/settings.gradle +++ b/apps/expo-go/android/settings.gradle @@ -40,7 +40,6 @@ expoAutolinking.exclude = [ 'expo-maps', 'expo-network-addons', 'expo-splash-screen', - 'expo-blob', '@expo/ui', 'expo-mesh-gradient', '@expo/app-integrity', diff --git a/apps/expo-go/ios/Client/AppDelegate.swift b/apps/expo-go/ios/Client/AppDelegate.swift index a80a0a2ab01271..243d94366884f9 100644 --- a/apps/expo-go/ios/Client/AppDelegate.swift +++ b/apps/expo-go/ios/Client/AppDelegate.swift @@ -48,6 +48,9 @@ class AppDelegate: ExpoAppDelegate { window.backgroundColor = UIColor.white rootViewController = (ExpoKit.sharedInstance().rootViewController() as! EXRootViewController) window.rootViewController = rootViewController + if let initialURL = EXKernelLinkingManager.initialUrl(fromLaunchOptions: launchOptions) { + rootViewController?.setInitialHomeURL(initialURL) + } window.makeKeyAndVisible() } diff --git a/apps/expo-go/ios/Client/EXRootViewController.h b/apps/expo-go/ios/Client/EXRootViewController.h index 23c432bf9b0cc2..0ba46b97c87467 100644 --- a/apps/expo-go/ios/Client/EXRootViewController.h +++ b/apps/expo-go/ios/Client/EXRootViewController.h @@ -4,4 +4,6 @@ @interface EXRootViewController : EXViewController +- (void)setInitialHomeURL:(NSURL *)url; + @end diff --git a/apps/expo-go/ios/Client/EXRootViewController.m b/apps/expo-go/ios/Client/EXRootViewController.m index 2dd44d09c9f493..3830a1a84a63e9 100644 --- a/apps/expo-go/ios/Client/EXRootViewController.m +++ b/apps/expo-go/ios/Client/EXRootViewController.m @@ -31,6 +31,7 @@ @interface EXRootViewController () @property (nonatomic, weak) UIViewController *transitioningToViewController; @property (nonatomic, readonly) BOOL isLocalNetworkAccessGranted; @property (nonatomic, strong) HomeViewController *homeViewController; +@property (nonatomic, strong) NSURL *pendingInitialHomeURL; @end @@ -45,6 +46,31 @@ - (instancetype)init return self; } +- (BOOL)canBecomeFirstResponder +{ + return YES; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self becomeFirstResponder]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + [self resignFirstResponder]; +} + +- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent * _Nullable)event +{ + [super motionEnded:motion withEvent:event]; + if (motion == UIEventSubtypeMotionShake && [DevMenuManager.shared getMotionGestureEnabled]) { + [DevMenuManager.shared toggleMenu]; + } +} + #pragma mark - Screen Orientation - (BOOL)shouldAutorotate @@ -87,9 +113,22 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations - (void)createRootAppAndMakeVisible { _homeViewController = [[HomeViewController alloc] init]; + if (_pendingInitialHomeURL) { + _homeViewController.initialURL = _pendingInitialHomeURL; + } [self _showHomeViewController]; } +#pragma mark - Initial URL + +- (void)setInitialHomeURL:(NSURL *)url +{ + _pendingInitialHomeURL = url; + if (_homeViewController != nil) { + _homeViewController.initialURL = url; + } +} + #pragma mark - EXAppBrowserController - (void)moveAppToVisible:(EXKernelAppRecord *)appRecord @@ -173,6 +212,9 @@ - (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(EXManifestsManifest NSDictionary *expoClient = extra[@"expoClient"]; appName = expoClient[@"name"]; iconUrl = expoClient[@"iconUrl"]; + if (!iconUrl && [expoClient[@"icon"] isKindOfClass:[NSString class]]) { + iconUrl = expoClient[@"icon"]; + } } } @@ -180,9 +222,32 @@ - (void)addHistoryItemWithUrl:(NSURL *)manifestUrl manifest:(EXManifestsManifest appName = manifest.rawManifestJSON[@"name"]; } + if (!iconUrl && manifest.rawManifestJSON[@"iconUrl"]) { + iconUrl = manifest.rawManifestJSON[@"iconUrl"]; + } + if (!iconUrl && manifest.rawManifestJSON[@"icon"]) { + iconUrl = manifest.rawManifestJSON[@"icon"]; + } + if (!iconUrl && [manifest.rawManifestJSON[@"ios"] isKindOfClass:[NSDictionary class]]) { + NSDictionary *iosConfig = manifest.rawManifestJSON[@"ios"]; + if (iosConfig[@"iconUrl"]) { + iconUrl = iosConfig[@"iconUrl"]; + } else if (iosConfig[@"icon"]) { + iconUrl = iosConfig[@"icon"]; + } + } + if (!appName) { appName = manifestUrl.absoluteString; } + + if (iconUrl && [iconUrl length] > 0) { + NSURL *resolved = [NSURL URLWithString:iconUrl]; + if (resolved == nil || resolved.scheme == nil) { + resolved = [NSURL URLWithString:iconUrl relativeToURL:manifestUrl]; + } + iconUrl = resolved.absoluteString; + } [[ExpoGoHomeBridge shared] addHistoryItemWithUrl:manifestUrl.absoluteString name:appName diff --git a/apps/expo-go/ios/Client/SwiftUI/Account/AccountSheet.swift b/apps/expo-go/ios/Client/SwiftUI/Account/AccountSheet.swift index e65c69a8b80676..4489581a386a0e 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Account/AccountSheet.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Account/AccountSheet.swift @@ -1,6 +1,7 @@ // Copyright ยฉ 2025 650 Industries. All rights reserved. import SwiftUI +import UIKit struct AccountSheet: View { @Environment(\.dismiss) private var dismiss @@ -44,8 +45,7 @@ struct AccountSheet: View { Button { dismiss() - } - label: { + } label: { Image(systemName: "xmark") .font(.system(size: 16, weight: .medium)) .foregroundColor(.primary) @@ -75,19 +75,19 @@ struct AccountSheet: View { } } - Button { - viewModel.signOut() - } - label: { - Text("Logout") - .font(.headline) - .fontWeight(.bold) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - } - .background(Color.black) - .cornerRadius(12) + Button { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + viewModel.signOut() + } label: { + Text("Logout") + .font(.headline) + .fontWeight(.bold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + } + .background(Color.black) + .cornerRadius(12) } } @@ -111,11 +111,11 @@ struct AccountSheet: View { private var signInButton: some View { Button { + UIImpactFeedbackGenerator(style: .light).impactOccurred() Task { await viewModel.signIn() } - } - label: { + } label: { Text("Log In") .font(.headline) .fontWeight(.semibold) @@ -130,11 +130,11 @@ struct AccountSheet: View { private var signUpButton: some View { Button { + UIImpactFeedbackGenerator(style: .light).impactOccurred() Task { await viewModel.signUp() } - } - label: { + } label: { Text("Sign Up") .font(.headline) .fontWeight(.semibold) @@ -142,16 +142,16 @@ struct AccountSheet: View { .frame(maxWidth: .infinity) .padding(.vertical, 12) } - .background(Color(.white)) + .background(Color.white) .cornerRadius(12) .disabled(viewModel.isAuthenticating) } private func accountRow(account: Account) -> some View { Button { + UIImpactFeedbackGenerator(style: .light).impactOccurred() viewModel.selectAccount(accountId: account.id) - } - label: { + } label: { HStack(spacing: 12) { AvatarView(account: account, size: 40) diff --git a/apps/expo-go/ios/Client/SwiftUI/DesignSystem.swift b/apps/expo-go/ios/Client/SwiftUI/DesignSystem.swift index 882bb9c6e4992f..b2e088eebd6471 100644 --- a/apps/expo-go/ios/Client/SwiftUI/DesignSystem.swift +++ b/apps/expo-go/ios/Client/SwiftUI/DesignSystem.swift @@ -4,165 +4,29 @@ import SwiftUI import ExpoModulesCore extension Color { - // Brand colors static let expoBlue = Color(red: 0.0, green: 0.46, blue: 1.0) - static let expoGreen = Color(red: 0.0, green: 0.8, blue: 0.4) - static let expoOrange = Color(red: 1.0, green: 0.4, blue: 0.0) - static let expoRed = Color(red: 1.0, green: 0.23, blue: 0.19) - static let expoPink = Color(red: 1.0, green: 0.18, blue: 0.58) - static let expoPurple = Color(red: 0.56, green: 0.27, blue: 1.0) - static let expoPrimaryText = Color(.label) static let expoSecondaryText = Color(.secondaryLabel) - static let expoTertiaryText = Color(.tertiaryLabel) - - static let expoBorder = Color(.separator) - static let expoSecondaryBorder = Color(.opaqueSeparator) static let expoSystemBackground = Color(uiColor: .systemBackground) static let expoSecondarySystemBackground = Color(uiColor: .secondarySystemBackground) - static let expoTertiarySystemBackground = Color(uiColor: .tertiarySystemBackground) - static let expoSystemGroupedBackground = Color(uiColor: .systemGroupedBackground) static let expoSecondarySystemGroupedBackground = Color(uiColor: .secondarySystemGroupedBackground) - static let expoTertiarySystemGroupedBackground = Color(uiColor: .tertiarySystemGroupedBackground) - static let expoSystemGray = Color(uiColor: .systemGray) - static let expoSystemGray2 = Color(uiColor: .systemGray2) - static let expoSystemGray3 = Color(uiColor: .systemGray3) static let expoSystemGray4 = Color(uiColor: .systemGray4) static let expoSystemGray5 = Color(uiColor: .systemGray5) static let expoSystemGray6 = Color(uiColor: .systemGray6) } extension Font { - static func expoTitle(_ size: CGFloat = 28) -> Font { - return .system(size: size, weight: .bold, design: .default) - } - - static func expoHeadline(_ size: CGFloat = 17) -> Font { - return .system(size: size, weight: .semibold, design: .default) - } - - static func expoBody(_ size: CGFloat = 17) -> Font { - return .system(size: size, weight: .regular, design: .default) - } - static func expoCaption(_ size: CGFloat = 12) -> Font { return .system(size: size, weight: .medium, design: .default) } - - static func expoFootnote(_ size: CGFloat = 13) -> Font { - return .system(size: size, weight: .regular, design: .default) - } -} - -struct Spacing { - static let xxs: CGFloat = 2 - static let xs: CGFloat = 4 - static let small: CGFloat = 8 - static let medium: CGFloat = 12 - static let large: CGFloat = 16 - static let xl: CGFloat = 20 - static let xxl: CGFloat = 24 - static let xxxl: CGFloat = 32 } struct BorderRadius { static let small: CGFloat = 4 static let medium: CGFloat = 8 static let large: CGFloat = 12 - static let xl: CGFloat = 16 - static let xxl: CGFloat = 20 -} - -extension View { - func expoCardShadow() -> some View { - self.shadow( - color: Color.black.opacity(0.08), - radius: 8, - x: 0, - y: 2 - ) - } - - func expoButtonShadow() -> some View { - self.shadow( - color: Color.black.opacity(0.1), - radius: 4, - x: 0, - y: 2 - ) - } - - func expoRowStyle() -> some View { - self - .padding() - .background(Color.expoSecondarySystemBackground) - .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) - } -} - -struct ExpoButtonStyle: ButtonStyle { - let style: ExpoButtonVariant - - enum ExpoButtonVariant { - case primary - case secondary - case ghost - case danger - } - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.expoHeadline()) - .padding(.horizontal, Spacing.large) - .padding(.vertical, Spacing.medium) - .background(backgroundColor) - .foregroundColor(textColor) - .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) - .scaleEffect(configuration.isPressed ? 0.98 : 1.0) - .opacity(configuration.isPressed ? 0.8 : 1.0) - .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) - } - - private var backgroundColor: Color { - switch style { - case .primary: - return .expoBlue - case .secondary: - return .expoSecondarySystemBackground - case .ghost: - return .clear - case .danger: - return .expoRed - } - } - - private var textColor: Color { - switch style { - case .primary, .danger: - return .white - case .secondary: - return .expoPrimaryText - case .ghost: - return .expoBlue - } - } -} - -struct ExpoCardStyle: ViewModifier { - func body(content: Content) -> some View { - content - .background(Color.expoSecondarySystemGroupedBackground) - .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) - .expoCardShadow() - } -} - -extension View { - func expoCard() -> some View { - self.modifier(ExpoCardStyle()) - } } struct ExpoSectionHeaderStyle: ViewModifier { @@ -179,71 +43,3 @@ extension View { self.modifier(ExpoSectionHeaderStyle()) } } - -struct ExpoListRowStyle: ViewModifier { - let isFirst: Bool - let isLast: Bool - - init(isFirst: Bool = false, isLast: Bool = false) { - self.isFirst = isFirst - self.isLast = isLast - } - - func body(content: Content) -> some View { - content - .padding(Spacing.large) - .background(Color.expoSecondarySystemGroupedBackground) - .overlay( - Rectangle() - .frame(height: 0.5) - .foregroundColor(.expoBorder) - .opacity(isFirst ? 0 : 1), - alignment: .top - ) - .clipShape( - RoundedCorners( - topLeading: isFirst ? BorderRadius.large : 0, - topTrailing: isFirst ? BorderRadius.large : 0, - bottomLeading: isLast ? BorderRadius.large : 0, - bottomTrailing: isLast ? BorderRadius.large : 0 - ) - ) - } -} - -extension View { - func expoListRow(isFirst: Bool = false, isLast: Bool = false) -> some View { - self.modifier(ExpoListRowStyle(isFirst: isFirst, isLast: isLast)) - } -} - -struct RoundedCorners: Shape { - let topLeading: CGFloat - let topTrailing: CGFloat - let bottomLeading: CGFloat - let bottomTrailing: CGFloat - - func path(in rect: CGRect) -> Path { - var path = Path() - - let width = rect.size.width - let height = rect.size.height - - path.move(to: CGPoint(x: topLeading, y: 0)) - - path.addLine(to: CGPoint(x: width - topTrailing, y: 0)) - path.addArc(center: CGPoint(x: width - topTrailing, y: topTrailing), radius: topTrailing, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false) - - path.addLine(to: CGPoint(x: width, y: height - bottomTrailing)) - path.addArc(center: CGPoint(x: width - bottomTrailing, y: height - bottomTrailing), radius: bottomTrailing, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false) - - path.addLine(to: CGPoint(x: bottomLeading, y: height)) - path.addArc(center: CGPoint(x: bottomLeading, y: height - bottomLeading), radius: bottomLeading, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false) - - path.addLine(to: CGPoint(x: 0, y: topLeading)) - path.addArc(center: CGPoint(x: topLeading, y: topLeading), radius: topLeading, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false) - - path.closeSubpath() - return path - } -} diff --git a/apps/expo-go/ios/Client/SwiftUI/DiagnosticsTabView.swift b/apps/expo-go/ios/Client/SwiftUI/DiagnosticsTabView.swift index 96f328d425b288..a30db26cf36a44 100644 --- a/apps/expo-go/ios/Client/SwiftUI/DiagnosticsTabView.swift +++ b/apps/expo-go/ios/Client/SwiftUI/DiagnosticsTabView.swift @@ -11,8 +11,7 @@ struct DiagnosticsTabView: View { NavigationLink(destination: AudioDiagnosticsView()) { DiagnosticCard( title: "Audio", - description: "On iOS you can play audio in the foreground and background, choose whether it plays when the device is on silent, and set how the audio interacts with audio from other apps. This diagnostic allows you to see the available options.", - action: {} + description: "On iOS you can play audio in the foreground and background, choose whether it plays when the device is on silent, and set how the audio interacts with audio from other apps. This diagnostic allows you to see the available options." ) } .buttonStyle(PlainButtonStyle()) @@ -20,8 +19,7 @@ struct DiagnosticsTabView: View { NavigationLink(destination: LocationDiagnosticsView()) { DiagnosticCard( title: "Background location", - description: "On iOS it's possible to track your location when an app is foregrounded, backgrounded, or even closed. This diagnostic allows you to see what options are available, see the output, and test the functionality on your device. None of the location data will leave your device.", - action: {} + description: "On iOS it's possible to track your location when an app is foregrounded, backgrounded, or even closed. This diagnostic allows you to see what options are available, see the output, and test the functionality on your device. None of the location data will leave your device." ) } .buttonStyle(PlainButtonStyle()) @@ -29,8 +27,7 @@ struct DiagnosticsTabView: View { NavigationLink(destination: GeofencingDiagnosticsView()) { DiagnosticCard( title: "Geofencing", - description: "You can fire actions when your device enters specific geographical regions represented by a longitude, latitude, and a radius. This diagnostic lets you experiment with Geofencing using regions that you specify and shows you the data that is made available. None of the data will leave your device.", - action: {} + description: "You can fire actions when your device enters specific geographical regions represented by a longitude, latitude, and a radius. This diagnostic lets you experiment with Geofencing using regions that you specify and shows you the data that is made available. None of the data will leave your device." ) } .buttonStyle(PlainButtonStyle()) @@ -46,7 +43,6 @@ struct DiagnosticsTabView: View { struct DiagnosticCard: View { let title: String let description: String - let action: () -> Void var body: some View { HStack(alignment: .top) { diff --git a/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift b/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift index 5ba54ba41e4509..b60262d1f261af 100644 --- a/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift +++ b/apps/expo-go/ios/Client/SwiftUI/GraphQL/APIClient.swift @@ -27,6 +27,12 @@ class APIClient { : "https://exp.host/--/graphql" } + var apiOrigin: String { + return useStaging + ? "https://staging.exp.host" + : "https://exp.host" + } + var websiteOrigin: String { return useStaging ? "https://staging.expo.dev" diff --git a/apps/expo-go/ios/Client/SwiftUI/GraphQL/Errors.swift b/apps/expo-go/ios/Client/SwiftUI/GraphQL/Errors.swift index 08771f930a9035..60915a40ad4bce 100644 --- a/apps/expo-go/ios/Client/SwiftUI/GraphQL/Errors.swift +++ b/apps/expo-go/ios/Client/SwiftUI/GraphQL/Errors.swift @@ -2,7 +2,7 @@ import Foundation -enum APIError: Error { +enum APIError: LocalizedError { case invalidURL case invalidResponse case httpError(statusCode: Int, message: String) @@ -10,7 +10,7 @@ enum APIError: Error { case networkError(Error) case authenticationRequired - var localizedDescription: String { + var errorDescription: String? { switch self { case .invalidURL: return "Invalid URL" diff --git a/apps/expo-go/ios/Client/SwiftUI/GraphQL/Models.swift b/apps/expo-go/ios/Client/SwiftUI/GraphQL/Models.swift index e8e270f0ab4615..a5ef78572d497c 100644 --- a/apps/expo-go/ios/Client/SwiftUI/GraphQL/Models.swift +++ b/apps/expo-go/ios/Client/SwiftUI/GraphQL/Models.swift @@ -118,7 +118,7 @@ struct Branch: Codable { let updates: [AppUpdate] } -struct AppUpdate: Codable { +struct AppUpdate: Identifiable, Codable { let id: String let group: String? let message: String? @@ -179,6 +179,43 @@ struct ProjectDetail: Codable { let updateBranches: [BranchDetail] } +struct BranchesListResponse: Codable { + let data: BranchesListData +} + +struct BranchesListData: Codable { + let app: BranchesListApp +} + +struct BranchesListApp: Codable { + let byId: BranchesListProject +} + +struct BranchesListProject: Codable { + let id: String + let name: String + let updateBranches: [BranchDetail] + let updateBranchesCount: Int +} + +struct BranchDetailsResponse: Codable { + let data: BranchDetailsData +} + +struct BranchDetailsData: Codable { + let app: BranchDetailsApp +} + +struct BranchDetailsApp: Codable { + let byId: BranchDetailsProject +} + +struct BranchDetailsProject: Codable { + let id: String + let name: String + let updateBranchByName: BranchDetail? +} + struct BranchDetail: Identifiable, Codable { let id: String let name: String diff --git a/apps/expo-go/ios/Client/SwiftUI/GraphQL/Queries.swift b/apps/expo-go/ios/Client/SwiftUI/GraphQL/Queries.swift index 27d89f05e73199..00398684e7eb02 100644 --- a/apps/expo-go/ios/Client/SwiftUI/GraphQL/Queries.swift +++ b/apps/expo-go/ios/Client/SwiftUI/GraphQL/Queries.swift @@ -188,4 +188,59 @@ struct Queries { } """ } + + static func getBranchesList() -> String { + return """ + query BranchesListQuery($appId: String!, $limit: Int!, $offset: Int!, $platform: AppPlatform!) { + app { + byId(appId: $appId) { + id + name + updateBranches(limit: $limit, offset: $offset) { + id + name + updates(limit: 1, offset: 0, filter: { platform: $platform }) { + id + group + message + createdAt + runtimeVersion + expoGoSDKVersion + platform + manifestPermalink + } + } + updateBranchesCount + } + } + } + """ + } + + static func getBranchDetails() -> String { + return """ + query BranchDetailsQuery($appId: String!, $branchName: String!, $platform: AppPlatform!) { + app { + byId(appId: $appId) { + id + name + updateBranchByName(name: $branchName) { + id + name + updates(limit: 25, offset: 0, filter: { platform: $platform }) { + id + group + message + createdAt + runtimeVersion + expoGoSDKVersion + platform + manifestPermalink + } + } + } + } + } + """ + } } diff --git a/apps/expo-go/ios/Client/SwiftUI/HomeRootView.swift b/apps/expo-go/ios/Client/SwiftUI/HomeRootView.swift index 95d710800835d4..23f88d35f55ff5 100644 --- a/apps/expo-go/ios/Client/SwiftUI/HomeRootView.swift +++ b/apps/expo-go/ios/Client/SwiftUI/HomeRootView.swift @@ -3,16 +3,23 @@ import SwiftUI import UIKit +enum HomeTab: Hashable { + case home + case diagnostics + case settings +} + public struct HomeRootView: View { @ObservedObject var viewModel: HomeViewModel @State private var showingUserProfile = false + @State private var selectedTab: HomeTab = .home init(viewModel: HomeViewModel) { self.viewModel = viewModel } public var body: some View { - TabView { + TabView(selection: $selectedTab) { NavigationView { HomeTabView() } @@ -20,7 +27,7 @@ public struct HomeRootView: View { Image(systemName: "house.fill") Text("Home") } - .navigationBarHidden(true) + .tag(HomeTab.home) NavigationView { DiagnosticsTabView() @@ -29,12 +36,14 @@ public struct HomeRootView: View { Image(systemName: "stethoscope") Text("Diagnostics") } + .tag(HomeTab.diagnostics) - SettingsTabView() + SettingsTabView(selectedTab: $selectedTab) .tabItem { Image(systemName: "gearshape") Text("Settings") } + .tag(HomeTab.settings) } .environmentObject(viewModel) .environmentObject(ExpoGoNavigation(showingUserProfile: $showingUserProfile)) @@ -42,17 +51,12 @@ public struct HomeRootView: View { AccountSheet() .environmentObject(viewModel) } - .alert("Error", isPresented: Binding( - get: { viewModel.errorToShow != nil }, - set: { if !$0 { viewModel.clearError() } } - )) { - Button("OK") { - viewModel.clearError() - } - } message: { - if let error = viewModel.errorToShow { - Text(error.message) - } + .alert(item: $viewModel.errorToShow) { error in + Alert( + title: Text("Error"), + message: Text(error.message), + dismissButton: .default(Text("OK")) + ) } } } diff --git a/apps/expo-go/ios/Client/SwiftUI/HomeTabView.swift b/apps/expo-go/ios/Client/SwiftUI/HomeTabView.swift index 2dade31280b306..2463ef3b161652 100644 --- a/apps/expo-go/ios/Client/SwiftUI/HomeTabView.swift +++ b/apps/expo-go/ios/Client/SwiftUI/HomeTabView.swift @@ -4,6 +4,7 @@ import SwiftUI struct HomeTabView: View { @EnvironmentObject var viewModel: HomeViewModel + @StateObject private var reviewManager = UserReviewManager() var body: some View { VStack(spacing: 0) { @@ -11,6 +12,18 @@ struct HomeTabView: View { ScrollView { VStack(spacing: 20) { + NavigationLink(destination: FeedbackFormView(), isActive: $viewModel.showingFeedbackForm) { + EmptyView() + } + + if reviewManager.shouldShowReviewSection { + UserReviewSection(reviewManager: reviewManager) { + viewModel.showFeedbackForm() + } + } + + UpgradeWarningView() + DevServersSection() if !viewModel.recentlyOpenedApps.isEmpty { @@ -58,9 +71,17 @@ struct HomeTabView: View { } .onAppear { viewModel.onViewWillAppear() + reviewManager.recordHomeAppear() + reviewManager.updateCounts(apps: viewModel.projects.count, snacks: viewModel.snacks.count) } .onDisappear { viewModel.onViewDidDisappear() } + .onChange(of: viewModel.projects.count) { _ in + reviewManager.updateCounts(apps: viewModel.projects.count, snacks: viewModel.snacks.count) + } + .onChange(of: viewModel.snacks.count) { _ in + reviewManager.updateCounts(apps: viewModel.projects.count, snacks: viewModel.snacks.count) + } } } diff --git a/apps/expo-go/ios/Client/SwiftUI/HomeViewController.swift b/apps/expo-go/ios/Client/SwiftUI/HomeViewController.swift index c53dea1db0631a..c5ac4193d2227b 100644 --- a/apps/expo-go/ios/Client/SwiftUI/HomeViewController.swift +++ b/apps/expo-go/ios/Client/SwiftUI/HomeViewController.swift @@ -6,6 +6,14 @@ import SwiftUI @objc public class HomeViewController: UIViewController { private var hostingController: UIHostingController? var viewModel = HomeViewModel() + @objc public var initialURL: URL? { + didSet { + if isViewLoaded, view.window != nil { + handleInitialURLIfNeeded() + } + } + } + private var hasHandledInitialURL = false @objc public override init(nibName: String?, bundle: Bundle?) { super.init(nibName: nibName, bundle: bundle) @@ -63,4 +71,38 @@ import SwiftUI super.viewDidDisappear(animated) viewModel.onViewDidDisappear() } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + handleInitialURLIfNeeded() + } + + private func handleInitialURLIfNeeded() { + guard !hasHandledInitialURL, let initialURL else { + return + } + + hasHandledInitialURL = true + self.initialURL = nil + + guard shouldOpenInitialURL(initialURL) else { + return + } + + let expURL = toExpURLString(initialURL) + viewModel.openApp(url: expURL) + } + + private func shouldOpenInitialURL(_ url: URL) -> Bool { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let host = components.host else { + return true + } + + if (host == "expo.io" || host == "expo.dev") && components.path == "/expo-go" { + return false + } + + return true + } } diff --git a/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift b/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift index f388775651e9fb..0f4db9f383745f 100644 --- a/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift +++ b/apps/expo-go/ios/Client/SwiftUI/HomeViewModel.swift @@ -12,7 +12,7 @@ class HomeViewModel: ObservableObject { @Published var recentlyOpenedApps: [RecentlyOpenedApp] = [] - @Published var showingAccountSheet = false + @Published var showingFeedbackForm = false @Published var errorToShow: ErrorInfo? @Published var isNetworkAvailable = true @@ -65,6 +65,7 @@ class HomeViewModel: ObservableObject { func onViewWillAppear() { serverService.startDiscovery() + serverService.setSessionSecret(authService.sessionSecret) if isAuthenticated, let account = selectedAccount { dataService.startPolling(accountName: account.name) @@ -120,6 +121,7 @@ class HomeViewModel: ObservableObject { async let task = dataService.fetchProjectsAndData(accountName: account.name) serverService.discoverDevelopmentServers() + serverService.refreshRemoteSessions() await task } @@ -174,26 +176,6 @@ class HomeViewModel: ObservableObject { openAppViaBridge(url: url) } - func extractAppName(from url: String) -> String { - guard let urlComponents = URL(string: url) else { - return url - } - - let pathComponents = urlComponents.path.components(separatedBy: "/").filter { !$0.isEmpty } - if let lastComponent = pathComponents.last, !lastComponent.isEmpty, lastComponent != "@" { - return lastComponent - } - - if let host = urlComponents.host { - if let port = urlComponents.port { - return "\(host):\(port)" - } - return host - } - - return url - } - func updateShakeGesture(_ enabled: Bool) { settingsManager.updateShakeGesture(enabled) } @@ -206,18 +188,14 @@ class HomeViewModel: ObservableObject { settingsManager.updateTheme(themeIndex) } - func showAccountSheet() { - showingAccountSheet = true + func showFeedbackForm() { + showingFeedbackForm = true } func showError(_ message: String, apiError: APIError? = nil) { errorToShow = ErrorInfo(message: message, apiError: apiError) } - func clearError() { - errorToShow = nil - } - private func setupSubscriptions() { authService.$user .sink { [weak self] in self?.user = $0 } @@ -232,7 +210,10 @@ class HomeViewModel: ObservableObject { .store(in: &cancellables) authService.$isAuthenticated - .sink { [weak self] in self?.isAuthenticated = $0 } + .sink { [weak self] isAuthenticated in + self?.isAuthenticated = isAuthenticated + self?.serverService.setSessionSecret(self?.authService.sessionSecret) + } .store(in: &cancellables) dataService.$projects @@ -295,11 +276,12 @@ struct RecentlyOpenedApp: Identifiable, Codable { } struct DevelopmentServer: Identifiable { - var id = UUID() + var id: String { url } let url: String let description: String let source: String let isRunning: Bool + var iconUrl: String? } struct ExpoProject: Identifiable, Codable { diff --git a/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift b/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift index f131eea0344ee1..8940dc2b304b18 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Rows/BranchRow.swift @@ -10,43 +10,80 @@ struct BranchRow: View { Button { onTap() } label: { - HStack(spacing: 12) { - VStack(alignment: .leading, spacing: 4) { - Text(branch.name) + BranchRowContent(branch: branch) + } + .buttonStyle(PlainButtonStyle()) + } +} + +struct BranchRowContent: View { + let branch: BranchDetail + + var body: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + HStack { + Image("branch-icon") + Text("Branch: \(branch.name)") .font(.body) .fontWeight(.semibold) .foregroundColor(.primary) + } - if let update = branch.updates.first { - if let message = update.message, !message.isEmpty { - Text(message) + if let update = branch.updates.first { + if let message = update.message, !message.isEmpty { + HStack { + Image("update-icon") + Text("\"\(message)\"") .font(.caption) .foregroundColor(.secondary) .lineLimit(1) } - - if let sdkVersion = update.expoGoSDKVersion { - Text("SDK \(sdkVersion)") - .font(.caption2) - .foregroundColor(.secondary) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.expoSecondarySystemGroupedBackground) - .clipShape(RoundedRectangle(cornerRadius: BorderRadius.small)) - } } + + Text("Published \(formattedDate(update.createdAt))") + .font(.caption) + .foregroundColor(.secondary) } + } - Spacer() + Spacer() - Image(systemName: "chevron.right") - .font(.caption) - .foregroundColor(.secondary) + Image(systemName: "chevron.right") + .font(.caption) + .foregroundColor(.secondary) + } + .padding() + .background(Color.expoSecondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + } + + private func formattedDate(_ value: String) -> String { + let formatters = [ + isoFormatter(withFractionalSeconds: true), + isoFormatter(withFractionalSeconds: false) + ] + for formatter in formatters { + if let date = formatter.date(from: value) { + let display = DateFormatter() + display.locale = Locale(identifier: "en_US_POSIX") + display.dateFormat = "MMM d, yyyy h:mm a" + return display.string(from: date) } - .padding() - .background(Color.expoSecondarySystemBackground) - .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) } - .buttonStyle(PlainButtonStyle()) + return value + } + + private func isoFormatter(withFractionalSeconds: Bool) -> ISO8601DateFormatter { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [ + .withInternetDateTime, + .withDashSeparatorInDate, + .withColonSeparatorInTime + ] + if withFractionalSeconds { + formatter.formatOptions.insert(.withFractionalSeconds) + } + return formatter } } diff --git a/apps/expo-go/ios/Client/SwiftUI/Rows/DevServerRow.swift b/apps/expo-go/ios/Client/SwiftUI/Rows/DevServerRow.swift index d6b58f1c1470d4..691a32d8a1aeeb 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Rows/DevServerRow.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Rows/DevServerRow.swift @@ -12,12 +12,19 @@ struct DevServerRow: View { } label: { HStack { - Circle() - .fill(Color.green) - .frame(width: 12, height: 12) + DevServerIcon(source: server.source, iconUrl: server.iconUrl) - Text(server.description) - .foregroundColor(.primary) + VStack(alignment: .leading, spacing: 2) { + Text(server.description.isEmpty ? server.url : server.description) + .fontWeight(.semibold) + .foregroundColor(.primary) + + if server.description != server.url { + Text(server.url) + .font(.caption) + .foregroundColor(.secondary) + } + } Spacer() Image(systemName: "chevron.right") @@ -31,3 +38,78 @@ struct DevServerRow: View { .buttonStyle(PlainButtonStyle()) } } + +private struct DevServerIcon: View { + let source: String + let iconUrl: String? + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + let background = colorScheme == .dark ? Color.expoSecondarySystemGroupedBackground : Color.white + let imageName = source == "snack" ? "snack" : "cli" + + RemoteIconView(iconUrl: iconUrl, fallbackImageName: imageName) + .frame(width: 28, height: 28) + .padding(10) + .background(background) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.expoSystemGray4.opacity(0.6), lineWidth: 1) + ) + } +} + +private struct RemoteIconView: View { + let iconUrl: String? + let fallbackImageName: String + @State private var image: UIImage? + @State private var isLoading = false + + var body: some View { + Group { + if let image { + Image(uiImage: image) + .resizable() + .frame(width: 40, height: 40) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } else { + Image(fallbackImageName) + .resizable() + .frame(width: 40, height: 40) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + .task { + await loadImageIfNeeded() + } + } + + private func loadImageIfNeeded() async { + guard let iconUrl, let url = URL(string: iconUrl), !isLoading else { + return + } + isLoading = true + + do { + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [] + let session = URLSession(configuration: config) + let (data, _) = try await session.data(from: url) + if let uiImage = UIImage(data: data) { + await MainActor.run { + self.image = uiImage + self.isLoading = false + } + } else { + await MainActor.run { + self.isLoading = false + } + } + } catch { + await MainActor.run { + self.isLoading = false + } + } + } +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Rows/RecentlyOpenedAppRow.swift b/apps/expo-go/ios/Client/SwiftUI/Rows/RecentlyOpenedAppRow.swift index 511bef529afc45..fa7e47c766dde9 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Rows/RecentlyOpenedAppRow.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Rows/RecentlyOpenedAppRow.swift @@ -12,15 +12,7 @@ struct RecentlyOpenedAppRow: View { } label: { HStack(spacing: 12) { if let iconUrl = app.iconUrl, let url = URL(string: iconUrl) { - Avatar(url: url) { image in - image - .resizable() - .scaledToFill() - } placeholder: { - Color.clear - } - .frame(width: 40, height: 40) - .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) + RecentlyOpenedIconView(url: url) } Text(app.name) @@ -41,3 +33,60 @@ struct RecentlyOpenedAppRow: View { .buttonStyle(PlainButtonStyle()) } } + +private struct RecentlyOpenedIconView: View { + let url: URL + private let size: CGFloat = 40 + @State private var image: UIImage? + @State private var isLoading = false + + var body: some View { + Group { + if let image { + Image(uiImage: image) + .resizable() + .scaledToFill() + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) + } else { + RoundedRectangle(cornerRadius: BorderRadius.medium) + .fill(Color.expoSecondarySystemGroupedBackground) + .frame(width: size, height: size) + } + } + .task { + await loadImage() + } + } + + private func loadImage() async { + guard !isLoading else { return } + isLoading = true + + do { + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [] + let session = URLSession(configuration: config) + let (data, response) = try await session.data(from: url) + + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode), + !data.isEmpty, + let uiImage = UIImage(data: data) else { + await MainActor.run { + self.isLoading = false + } + return + } + + await MainActor.run { + self.image = uiImage + self.isLoading = false + } + } catch { + await MainActor.run { + self.isLoading = false + } + } + } +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Rows/UpdateRow.swift b/apps/expo-go/ios/Client/SwiftUI/Rows/UpdateRow.swift new file mode 100644 index 00000000000000..ee1dcff458091e --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Rows/UpdateRow.swift @@ -0,0 +1,92 @@ +// Copyright ยฉ 2025 650 Industries. All rights reserved. + +import SwiftUI + +struct UpdateRow: View { + let update: AppUpdate + let isCompatible: Bool + let onOpen: () -> Void + + var body: some View { + Button { + if isCompatible { + onOpen() + } + } label: { + HStack(alignment: .top, spacing: 8) { + Image("update-icon") + .foregroundColor(.secondary) + .padding(.top, 2) + + VStack(alignment: .leading, spacing: 6) { + Text(updateTitle(update)) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + + Text("Published \(formattedDate(update.createdAt))") + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + + if !isCompatible { + Text("Not compatible with this version of Expo Go") + .font(.caption) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + + Spacer() + + if isCompatible { + Image(systemName: "chevron.right") + .font(.caption) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(Color.expoSecondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + } + .buttonStyle(PlainButtonStyle()) + } + + private func formattedDate(_ value: String) -> String { + let formatters = [ + isoFormatter(withFractionalSeconds: true), + isoFormatter(withFractionalSeconds: false) + ] + for formatter in formatters { + if let date = formatter.date(from: value) { + let display = DateFormatter() + display.locale = Locale(identifier: "en_US_POSIX") + display.dateFormat = "MMM d, yyyy h:mm a" + return display.string(from: date) + } + } + return value + } + + private func updateTitle(_ update: AppUpdate) -> String { + if let message = update.message, !message.isEmpty { + return "\"\(message)\"" + } + return update.id + } + + private func isoFormatter(withFractionalSeconds: Bool) -> ISO8601DateFormatter { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [ + .withInternetDateTime, + .withDashSeparatorInDate, + .withColonSeparatorInTime + ] + if withFractionalSeconds { + formatter.formatOptions.insert(.withFractionalSeconds) + } + return formatter + } +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift index ff51a26b3abf51..00a5b0f7ea3fe4 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/AuthenticationService.swift @@ -13,7 +13,11 @@ class AuthenticationService: ObservableObject { private let sessionKey = "expo-session-secret" private let selectedAccountKey = "expo-selected-account-id" - private let presentationContext = ExpoGoAuthPresentationContext() + private let presentationContext = AuthPresentationContextProvider() + + var sessionSecret: String? { + UserDefaults.standard.string(forKey: sessionKey) + } var selectedAccount: Account? { guard let userData = user, @@ -157,7 +161,7 @@ class AuthenticationService: ObservableObject { } } -private class ExpoGoAuthPresentationContext: NSObject, ASWebAuthenticationPresentationContextProviding { +private class AuthPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { let window = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift index b2484e9c074f7a..e536674fb0aae3 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/DevelopmentServerService.swift @@ -2,6 +2,7 @@ import Foundation import Combine +import UIKit @MainActor class DevelopmentServerService: ObservableObject { @@ -9,10 +10,19 @@ class DevelopmentServerService: ObservableObject { private var discoveryCancellables = Set() private let discoveryInterval: TimeInterval = 2.0 + private let remoteRefreshInterval: TimeInterval = 10.0 + private let remoteCacheKey = "expo-dev-sessions-cache" + private var remoteFailureCount = 0 + private var nextRemoteFetchAllowedAt: Date = .distantPast + private var localServers: [DevelopmentServer] = [] + private var remoteServers: [DevelopmentServer] = [] + private var sessionSecret: String? func startDiscovery() { stopDiscovery() discoverDevelopmentServers() + loadCachedRemoteSessions() + refreshRemoteSessions() Timer.publish(every: discoveryInterval, on: .main, in: .common) .autoconnect() @@ -21,12 +31,25 @@ class DevelopmentServerService: ObservableObject { self?.discoverDevelopmentServers() } .store(in: &discoveryCancellables) + + Timer.publish(every: remoteRefreshInterval, on: .main, in: .common) + .autoconnect() + .receive(on: DispatchQueue.global(qos: .background)) + .sink { [weak self] _ in + self?.refreshRemoteSessions() + } + .store(in: &discoveryCancellables) } func stopDiscovery() { discoveryCancellables.removeAll() } + func setSessionSecret(_ sessionSecret: String?) { + self.sessionSecret = sessionSecret + refreshRemoteSessions() + } + func discoverDevelopmentServers() { Task { var discoveredServers: [DevelopmentServer] = [] @@ -50,11 +73,18 @@ class DevelopmentServerService: ObservableObject { } await MainActor.run { - self.developmentServers = discoveredServers.sorted { $0.url < $1.url } + self.localServers = discoveredServers + self.updateDevelopmentServers() } } } + func refreshRemoteSessions() { + Task { + await fetchRemoteSessions() + } + } + private func checkDevelopmentServer(url: String) async -> DevelopmentServer? { guard let statusURL = URL(string: "\(url)/status") else { return nil @@ -67,11 +97,14 @@ class DevelopmentServerService: ObservableObject { httpResponse.statusCode == 200 { if let statusString = String(data: data, encoding: .utf8), statusString.contains("packager-status:running") { + let manifestInfo = await fetchLocalManifestInfo(url: url) + let description = manifestInfo?.name ?? url return DevelopmentServer( url: url, - description: url, - source: "local", - isRunning: true + description: description, + source: "desktop", + isRunning: true, + iconUrl: manifestInfo?.iconUrl ) } } @@ -79,4 +112,255 @@ class DevelopmentServerService: ObservableObject { return nil } + + private func fetchLocalManifestInfo(url: String) async -> (name: String?, iconUrl: String?)? { + let manifestPaths = [ + "\(url)/manifest", + "\(url)/manifest?platform=ios" + ] + + for manifestPath in manifestPaths { + guard let manifestURL = URL(string: manifestPath) else { + continue + } + + var request = URLRequest(url: manifestURL) + request.setValue("application/expo+json,application/json", forHTTPHeaderField: "Accept") + request.setValue("ios", forHTTPHeaderField: "Expo-Platform") + request.setValue("client", forHTTPHeaderField: "Expo-Client-Environment") + request.setValue(EXVersions.sharedInstance().sdkVersion, forHTTPHeaderField: "Expo-SDK-Version") + + do { + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) else { + continue + } + + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + if let parsed = parseManifestInfo(json: json, baseUrl: url) { + return parsed + } + } + } catch {} + } + + return nil + } + + private func parseManifestInfo(json: [String: Any], baseUrl: String) -> (name: String?, iconUrl: String?)? { + var name: String? + var iconUrl: String? + + if let extra = json["extra"] as? [String: Any], + let expoClient = extra["expoClient"] as? [String: Any] { + if let expoName = expoClient["name"] as? String, !expoName.isEmpty { + name = expoName + } + if let expoIcon = expoClient["iconUrl"] as? String, !expoIcon.isEmpty { + iconUrl = absoluteIconUrl(expoIcon, baseUrl: baseUrl) + } + if iconUrl == nil, let expoIcon = expoClient["icon"] as? String, !expoIcon.isEmpty { + iconUrl = absoluteIconUrl(expoIcon, baseUrl: baseUrl) + } + } + + if name == nil, let manifestName = json["name"] as? String, !manifestName.isEmpty { + name = manifestName + } + + if iconUrl == nil, let manifestIcon = json["iconUrl"] as? String, !manifestIcon.isEmpty { + iconUrl = absoluteIconUrl(manifestIcon, baseUrl: baseUrl) + } + if iconUrl == nil, let manifestIcon = json["icon"] as? String, !manifestIcon.isEmpty { + iconUrl = absoluteIconUrl(manifestIcon, baseUrl: baseUrl) + } + if iconUrl == nil, + let iosConfig = json["ios"] as? [String: Any], + let iosIcon = iosConfig["iconUrl"] as? String ?? iosConfig["icon"] as? String, + !iosIcon.isEmpty { + iconUrl = absoluteIconUrl(iosIcon, baseUrl: baseUrl) + } + + return (name, iconUrl) + } + + private func fetchRemoteSessions() async { + guard Date() >= nextRemoteFetchAllowedAt else { + return + } + + guard let sessionSecret, !sessionSecret.isEmpty else { + await MainActor.run { + self.remoteServers = [] + self.updateDevelopmentServers() + } + return + } + + guard let url = URL(string: "\(APIClient.shared.apiOrigin)/--/api/v2/development-sessions") else { + return + } + + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue(sessionSecret, forHTTPHeaderField: "Expo-Session") + request.setValue("ios", forHTTPHeaderField: "Expo-Platform") + request.setValue(EXVersions.sharedInstance().sdkVersion, forHTTPHeaderField: "Expo-SDK-Version") + + do { + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) else { + applyRemoteFailureBackoff() + return + } + + let decoder = JSONDecoder() + let sessions: [DevSession] + if let response = try? decoder.decode(DevSessionsResponse.self, from: data) { + sessions = response.data + } else if let directSessions = try? decoder.decode([DevSession].self, from: data) { + sessions = directSessions + } else { + applyRemoteFailureBackoff() + return + } + + let mappedServers = sessions.map { session in + DevelopmentServer( + url: session.url, + description: session.description, + source: session.source, + isRunning: true, + iconUrl: session.iconUrl + ) + } + + await MainActor.run { + self.remoteServers = mappedServers + self.updateDevelopmentServers() + } + cacheRemoteSessions(sessions) + remoteFailureCount = 0 + nextRemoteFetchAllowedAt = .distantPast + } catch {} + } + + private func updateDevelopmentServers() { + var merged: [String: DevelopmentServer] = [:] + for server in localServers + remoteServers { + let key = dedupeKey(for: server) + if let existing = merged[key] { + merged[key] = preferredServer(existing: existing, candidate: server) + } else { + merged[key] = server + } + } + developmentServers = merged.values.sorted { $0.url < $1.url } + } + + private func dedupeKey(for server: DevelopmentServer) -> String { + if !server.description.isEmpty && server.description != server.url { + return normalizeDescriptionKey(server.description).lowercased() + } + return normalizeUrl(server.url).lowercased() + } + + private func preferredServer(existing: DevelopmentServer, candidate: DevelopmentServer) -> DevelopmentServer { + if isLocalhostURL(existing.url) && !isLocalhostURL(candidate.url) { + return candidate + } + if isLocalhostURL(candidate.url) && !isLocalhostURL(existing.url) { + return existing + } + return existing + } + + private func normalizeDescriptionKey(_ description: String) -> String { + if let range = description.range(of: " on ", options: .backwards) { + let prefix = String(description[.. Bool { + guard let components = URLComponents(string: url), + let host = components.host?.lowercased() else { + return false + } + return host == "localhost" || host == "127.0.0.1" + } + + private func cacheRemoteSessions(_ sessions: [DevSession]) { + let cache = DevSessionsCache( + timestamp: Date(), + sessions: sessions + ) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + if let data = try? encoder.encode(cache) { + UserDefaults.standard.set(data, forKey: remoteCacheKey) + } + } + + private func loadCachedRemoteSessions() { + guard let data = UserDefaults.standard.data(forKey: remoteCacheKey) else { + return + } + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + guard let cache = try? decoder.decode(DevSessionsCache.self, from: data) else { + return + } + let mappedServers = cache.sessions.map { session in + DevelopmentServer( + url: session.url, + description: session.description, + source: session.source, + isRunning: true, + iconUrl: session.iconUrl + ) + } + remoteServers = mappedServers + updateDevelopmentServers() + } + + private func applyRemoteFailureBackoff() { + remoteFailureCount += 1 + let cappedFailures = min(remoteFailureCount, 5) + let delay = min(pow(2.0, Double(cappedFailures)) * 2.0, 60.0) + nextRemoteFetchAllowedAt = Date().addingTimeInterval(delay) + } + + private func absoluteIconUrl(_ iconUrl: String, baseUrl: String) -> String? { + if URL(string: iconUrl)?.scheme != nil { + return iconUrl + } + guard let base = URL(string: baseUrl), + let absolute = URL(string: iconUrl, relativeTo: base) else { + return nil + } + return absolute.absoluteString + } +} + +private struct DevSessionsResponse: Codable { + let data: [DevSession] +} + +private struct DevSession: Codable { + let description: String + let url: String + let source: String + let iconUrl: String? +} + +private struct DevSessionsCache: Codable { + let timestamp: Date + let sessions: [DevSession] } diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/FeedbackService.swift b/apps/expo-go/ios/Client/SwiftUI/Services/FeedbackService.swift new file mode 100644 index 00000000000000..65567d9e2d8828 --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Services/FeedbackService.swift @@ -0,0 +1,73 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +import Foundation +import UIKit + +struct FeedbackService { + func submitFeedback(message: String, email: String?) async throws { + guard let url = URL(string: "\(APIClient.shared.apiOrigin)/--/api/v2/feedback/expo-go-send") else { + throw APIError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let payload = FeedbackPayload( + feedback: message, + email: email, + metadata: FeedbackMetadata( + os: "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", + model: UIDevice.current.model, + expoGoVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + ) + ) + + let encoder = JSONEncoder() + request.httpBody = try encoder.encode(payload) + + do { + let (data, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw APIError.invalidResponse + } + + guard (200..<300).contains(httpResponse.statusCode) else { + let message = parseAPIErrorMessage(from: data) ?? "Something went wrong." + throw APIError.httpError(statusCode: httpResponse.statusCode, message: message) + } + } catch let error as APIError { + throw error + } catch { + throw APIError.networkError(error) + } + } + + private func parseAPIErrorMessage(from data: Data) -> String? { + guard let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let errors = jsonObject["errors"] as? [[String: Any]], + let firstError = errors.first else { + return nil + } + + if let details = firstError["details"] as? [String: Any], + let message = details["message"] as? String { + return message + } + + return firstError["message"] as? String + } +} + +private struct FeedbackPayload: Encodable { + let feedback: String + let email: String? + let metadata: FeedbackMetadata +} + +private struct FeedbackMetadata: Encodable { + let os: String + let model: String + let expoGoVersion: String? +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift b/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift index 01c40f3549ac9c..be4d111e4c3748 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Services/SettingsManager.swift @@ -2,6 +2,7 @@ import Foundation import UIKit +import EXDevMenu @MainActor class SettingsManager: ObservableObject { @@ -19,11 +20,13 @@ class SettingsManager: ObservableObject { func updateShakeGesture(_ enabled: Bool) { shakeToShowDevMenu = enabled saveDevSetting(key: "shakeToShow", value: enabled) + DevMenuManager.shared.setMotionGestureEnabled(enabled) } func updateThreeFingerGesture(_ enabled: Bool) { threeFingerLongPressEnabled = enabled saveDevSetting(key: "threeFingerLongPress", value: enabled) + DevMenuManager.shared.setTouchGestureEnabled(enabled) } func updateTheme(_ themeIndex: Int) { @@ -33,9 +36,8 @@ class SettingsManager: ObservableObject { } private func loadDevSettings() { - let devMenuDefaults = UserDefaults.standard.dictionary(forKey: "RCTDevMenu") ?? [:] - shakeToShowDevMenu = devMenuDefaults["shakeToShow"] as? Bool ?? true - threeFingerLongPressEnabled = devMenuDefaults["threeFingerLongPress"] as? Bool ?? true + shakeToShowDevMenu = DevMenuManager.shared.getMotionGestureEnabled() + threeFingerLongPressEnabled = DevMenuManager.shared.getTouchGestureEnabled() } private func saveDevSetting(key: String, value: Bool) { diff --git a/apps/expo-go/ios/Client/SwiftUI/Services/UserReviewManager.swift b/apps/expo-go/ios/Client/SwiftUI/Services/UserReviewManager.swift new file mode 100644 index 00000000000000..bee7118ff109ad --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Services/UserReviewManager.swift @@ -0,0 +1,142 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +import Foundation +import StoreKit +import UIKit + +@MainActor +final class UserReviewManager: ObservableObject { + @Published private(set) var shouldShowReviewSection = false + + private let storageKey = "userReviewInfo" + private let lastCrashKey = "EXKernelLastFatalErrorDateDefaultsKey" + private var info = UserReviewInfo() + private var appsCount = 0 + private var snacksCount = 0 + private var lastCrashDate: Date? + + init() { + loadInfo() + loadLastCrashDate() + refreshShouldShow() + NotificationCenter.default.addObserver( + self, + selector: #selector(handleWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + func recordHomeAppear() { + info.appOpenedCounter += 1 + saveInfo() + refreshShouldShow() + } + + func updateCounts(apps: Int, snacks: Int) { + appsCount = apps + snacksCount = snacks + refreshShouldShow() + } + + func dismissReviewSection() { + info.lastDismissDate = Date() + saveInfo() + refreshShouldShow() + } + + func requestReview() { + info.askedForNativeReviewDate = Date() + saveInfo() + refreshShouldShow() + requestStoreReview() + } + + func provideFeedback() { + info.showFeedbackFormDate = Date() + saveInfo() + refreshShouldShow() + } + + private func refreshShouldShow() { + let now = Date() + let noRecentDismisses = info.lastDismissDate.map { + now.timeIntervalSince($0) > 15 * 24 * 60 * 60 + } ?? true + let noRecentCrashes = lastCrashDate.map { + now.timeIntervalSince($0) > 60 * 60 + } ?? true + let hasAskedForReview = info.askedForNativeReviewDate != nil + let hasFeedbackForm = info.showFeedbackFormDate != nil + let meetsUsageThreshold = info.appOpenedCounter >= 50 || appsCount >= 5 || snacksCount >= 5 + + shouldShowReviewSection = noRecentDismisses + && !hasAskedForReview + && !hasFeedbackForm + && noRecentCrashes + && meetsUsageThreshold + } + + private func requestStoreReview() { + if let scene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first(where: { $0.activationState == .foregroundActive }) { + SKStoreReviewController.requestReview(in: scene) + } else { + SKStoreReviewController.requestReview() + } + } + + private func loadInfo() { + let storedData: Data? + if let data = UserDefaults.standard.data(forKey: storageKey) { + storedData = data + } else if let string = UserDefaults.standard.string(forKey: storageKey) { + storedData = string.data(using: .utf8) + } else { + storedData = nil + } + + guard let storedData else { + info = UserReviewInfo() + return + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + if let decoded = try? decoder.decode(UserReviewInfo.self, from: storedData) { + info = decoded + } else { + info = UserReviewInfo() + } + } + + @objc private func handleWillEnterForeground() { + loadLastCrashDate() + refreshShouldShow() + } + + private func loadLastCrashDate() { + if let date = UserDefaults.standard.object(forKey: lastCrashKey) as? Date { + lastCrashDate = date + } else { + lastCrashDate = nil + } + } + + private func saveInfo() { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + if let data = try? encoder.encode(info), + let string = String(data: data, encoding: .utf8) { + UserDefaults.standard.set(string, forKey: storageKey) + } + } +} + +private struct UserReviewInfo: Codable { + var askedForNativeReviewDate: Date? + var lastDismissDate: Date? + var showFeedbackFormDate: Date? + var appOpenedCounter: Int = 0 +} diff --git a/apps/expo-go/ios/Client/SwiftUI/SettingsTabView.swift b/apps/expo-go/ios/Client/SwiftUI/SettingsTabView.swift index 56d2313924f60f..00de74c57bbb89 100644 --- a/apps/expo-go/ios/Client/SwiftUI/SettingsTabView.swift +++ b/apps/expo-go/ios/Client/SwiftUI/SettingsTabView.swift @@ -2,13 +2,17 @@ import SwiftUI import AuthenticationServices +import AppTrackingTransparency struct SettingsTabView: View { + @Binding var selectedTab: HomeTab @EnvironmentObject var viewModel: HomeViewModel - @State private var allowAnalytics = false + @State private var shouldShowTrackingSection = false + @State private var isTrackingRequestInFlight = false @State private var isDeleting = false @State private var deletionError: String? - @State private var context: AuthPresentationContextProvider? + @State private var authSession: ASWebAuthenticationSession? + private let context = AuthPresentationContextProvider() var body: some View { ScrollView { @@ -76,14 +80,15 @@ struct SettingsTabView: View { .foregroundColor(.secondary) } - VStack(alignment: .leading, spacing: 16) { + if shouldShowTrackingSection { + VStack(alignment: .leading, spacing: 16) { Text("Tracking") .font(.headline) .foregroundColor(.primary) VStack(spacing: 0) { Button { - allowAnalytics.toggle() + requestTrackingPermission() } label: { HStack { Text("Allow access to app-related data for tracking") @@ -93,13 +98,13 @@ struct SettingsTabView: View { Spacer() - if allowAnalytics { - Image(systemName: "checkmark") - .foregroundColor(.expoBlue) + if isTrackingRequestInFlight { + ProgressView() } } .padding() } + .disabled(isTrackingRequestInFlight) .buttonStyle(PlainButtonStyle()) } .background(Color.expoSecondarySystemBackground) @@ -111,6 +116,7 @@ struct SettingsTabView: View { .foregroundColor(.expoBlue) } } + } VStack(alignment: .leading, spacing: 16) { Text("App Info") .font(.headline) @@ -129,7 +135,7 @@ struct SettingsTabView: View { } .frame(maxWidth: .infinity) .padding() - .background(Color(.secondarySystemBackground)) + .background(Color.expoSecondarySystemBackground) .foregroundColor(.primary) .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) } @@ -191,6 +197,9 @@ struct SettingsTabView: View { .background(Color.expoSystemBackground) .navigationTitle("Settings") .navigationBarTitleDisplayMode(.inline) + .task { + await refreshTrackingStatus() + } } private func getExpoSDKVersion() -> String { @@ -222,6 +231,7 @@ struct SettingsTabView: View { } let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "expauth") { [self] callbackURL, error in + authSession = nil isDeleting = false if let error { @@ -234,13 +244,34 @@ struct SettingsTabView: View { if callbackURL != nil { viewModel.signOut() + selectedTab = .home } } session.presentationContextProvider = context session.prefersEphemeralWebBrowserSession = false + authSession = session session.start() } + + private func refreshTrackingStatus() async { + let status = ATTrackingManager.trackingAuthorizationStatus + await MainActor.run { + shouldShowTrackingSection = (status == .notDetermined) + } + } + + private func requestTrackingPermission() { + guard !isTrackingRequestInFlight else { return } + isTrackingRequestInFlight = true + + ATTrackingManager.requestTrackingAuthorization { status in + DispatchQueue.main.async { + isTrackingRequestInFlight = false + shouldShowTrackingSection = (status == .notDetermined) + } + } + } } private class AuthPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding { diff --git a/apps/expo-go/ios/Client/SwiftUI/Utils/UrlUtils.swift b/apps/expo-go/ios/Client/SwiftUI/Utils/UrlUtils.swift index 42012abd62ad67..0eac35a7e3ccaa 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Utils/UrlUtils.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Utils/UrlUtils.swift @@ -27,7 +27,11 @@ func sanitizeUrlString(_ urlString: String) -> String? { var sanitizedUrl = urlString.trimmingCharacters(in: .whitespacesAndNewlines) if !sanitizedUrl.contains("://") { - sanitizedUrl = "http://" + sanitizedUrl + if sanitizedUrl.hasPrefix("@") { + sanitizedUrl = "exp://exp.host/" + sanitizedUrl + } else { + sanitizedUrl = "http://" + sanitizedUrl + } } guard URL(string: sanitizedUrl) != nil else { @@ -36,3 +40,20 @@ func sanitizeUrlString(_ urlString: String) -> String? { return sanitizedUrl } + +func toExpURLString(_ url: URL) -> String { + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return url.absoluteString + } + + if let scheme = components.scheme?.lowercased(), scheme == "exp" || scheme == "exps" { + return url.absoluteString + } + + if let scheme = components.scheme?.lowercased(), scheme == "http" || scheme == "https" { + components.scheme = "exp" + return components.url?.absoluteString ?? url.absoluteString + } + + return url.absoluteString +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/BranchDetailsView.swift b/apps/expo-go/ios/Client/SwiftUI/Views/BranchDetailsView.swift new file mode 100644 index 00000000000000..9da36bbf2fadc9 --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Views/BranchDetailsView.swift @@ -0,0 +1,174 @@ +// Copyright ยฉ 2025 650 Industries. All rights reserved. + +import SwiftUI + +struct BranchDetailsView: View { + let projectId: String + let branchName: String + @StateObject private var viewModel: BranchDetailsViewModel + @EnvironmentObject var homeViewModel: HomeViewModel + + init(projectId: String, branchName: String) { + self.projectId = projectId + self.branchName = branchName + self._viewModel = StateObject(wrappedValue: BranchDetailsViewModel(projectId: projectId, branchName: branchName)) + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + if viewModel.isLoading && viewModel.branch == nil { + ProgressView() + .frame(maxWidth: .infinity) + .padding(40) + } else if let branch = viewModel.branch { + BranchDetailsHeader(branchName: branchName, latestUpdate: branch.updates.first) { + openUpdate(branch.updates.first) + } + + VStack(alignment: .leading, spacing: 12) { + SectionHeader(title: "UPDATES") + + if branch.updates.isEmpty { + EmptyStateView( + icon: "arrow.branch", + message: "No updates", + description: "Publish an update to see it here" + ) + } else { + VStack(spacing: 6) { + ForEach(branch.updates) { update in + let compatible = isSDKCompatible(update.expoGoSDKVersion) + UpdateRow(update: update, isCompatible: compatible) { + openUpdate(update) + } + } + } + } + } + } else if viewModel.error != nil { + EmptyStateView( + icon: "exclamationmark.triangle", + message: "Failed to load branch", + description: "An unexpected error occurred", + actionTitle: "Try Again", + action: { + Task { + await viewModel.loadBranch() + } + } + ) + } + } + .padding() + } + .background(Color.expoSystemBackground) + .navigationTitle("Branch") + .navigationBarTitleDisplayMode(.inline) + .refreshable { + await viewModel.refresh() + } + .task { + await viewModel.loadBranch() + } + } + + private func openUpdate(_ update: AppUpdate?) { + guard let update else { + homeViewModel.showError("This branch has no published updates") + return + } + + guard isSDKCompatible(update.expoGoSDKVersion) else { + let updateSDK = update.expoGoSDKVersion ?? "unknown" + homeViewModel.showError("Selected update uses unsupported SDK (\(updateSDK))") + return + } + + homeViewModel.openApp(url: update.manifestPermalink) + homeViewModel.addToRecentlyOpened( + url: update.manifestPermalink, + name: "\(viewModel.projectName) - \(branchName)", + iconUrl: nil + ) + } +} + +struct BranchDetailsHeader: View { + let branchName: String + let latestUpdate: AppUpdate? + let onOpen: () -> Void + + var body: some View { + HStack { + HStack(spacing: 8) { + Image("branch-icon") + .foregroundColor(.primary) + Text(branchName) + .font(.headline) + } + + Spacer() + + if let latestUpdate, isSDKCompatible(latestUpdate.expoGoSDKVersion) { + Button("Open") { + onOpen() + } + .font(.subheadline) + .foregroundColor(.primary) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.expoSecondarySystemGroupedBackground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) + } + } + .padding() + .background(Color.expoSecondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + } +} + +@MainActor +class BranchDetailsViewModel: ObservableObject { + @Published var branch: BranchDetail? + @Published var projectName = "Project" + @Published var isLoading = false + @Published var error: Error? + + private let projectId: String + private let branchName: String + private var hasLoadedRemote = false + + init(projectId: String, branchName: String) { + self.projectId = projectId + self.branchName = branchName + } + + func loadBranch() async { + if hasLoadedRemote { return } + isLoading = true + defer { isLoading = false } + + do { + let response: BranchDetailsResponse = try await APIClient.shared.request( + Queries.getBranchDetails(), + variables: [ + "appId": projectId, + "branchName": branchName, + "platform": "IOS" + ] + ) + + projectName = response.data.app.byId.name + branch = response.data.app.byId.updateBranchByName + hasLoadedRemote = true + } catch { + self.error = error + } + } + + func refresh() async { + hasLoadedRemote = false + await loadBranch() + } +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/BranchesListView.swift b/apps/expo-go/ios/Client/SwiftUI/Views/BranchesListView.swift new file mode 100644 index 00000000000000..d2884f1066ae82 --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Views/BranchesListView.swift @@ -0,0 +1,137 @@ +// Copyright ยฉ 2025 650 Industries. All rights reserved. + +import SwiftUI + +struct BranchesListView: View { + let projectId: String + @StateObject private var viewModel: BranchesListViewModel + + init(projectId: String) { + self.projectId = projectId + self._viewModel = StateObject(wrappedValue: BranchesListViewModel(projectId: projectId)) + } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + if viewModel.isLoading && viewModel.branches.isEmpty { + ProgressView() + .frame(maxWidth: .infinity) + .padding(40) + } else if viewModel.branches.isEmpty { + EmptyStateView( + icon: "arrow.branch", + message: "No branches", + description: "Push updates to create branches" + ) + } else { + VStack(spacing: 6) { + ForEach(viewModel.branches) { branch in + NavigationLink(destination: BranchDetailsView(projectId: projectId, branchName: branch.name)) { + BranchRowContent(branch: branch) + } + .buttonStyle(PlainButtonStyle()) + } + + if viewModel.hasMore && !viewModel.isLoading { + Button("Load More") { + Task { + await viewModel.loadMore() + } + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.expoSecondarySystemGroupedBackground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + } + + if viewModel.isLoading && !viewModel.branches.isEmpty { + ProgressView() + .padding() + } + } + } + } + .padding() + } + .background(Color.expoSystemBackground) + .navigationTitle("Branches") + .navigationBarTitleDisplayMode(.inline) + .task { + await viewModel.loadInitial() + } + .alert("Error", isPresented: $viewModel.showingError) { + Button("OK") { + viewModel.showingError = false + } + } message: { + if let error = viewModel.error { + Text(error.localizedDescription) + } + } + } + +} + +@MainActor +class BranchesListViewModel: ObservableObject { + @Published var branches: [BranchDetail] = [] + @Published var isLoading = false + @Published var showingError = false + @Published var error: Error? + @Published var hasMore = false + @Published var projectName = "Project" + + private let projectId: String + private var currentOffset = 0 + private let pageSize = 25 + private var totalCount = 0 + + init(projectId: String) { + self.projectId = projectId + } + + func loadInitial() async { + guard branches.isEmpty else { return } + currentOffset = 0 + await fetchBranches() + } + + func loadMore() async { + guard !isLoading, hasMore else { return } + currentOffset += pageSize + await fetchBranches() + } + + private func fetchBranches() async { + isLoading = true + defer { isLoading = false } + + do { + let response: BranchesListResponse = try await APIClient.shared.request( + Queries.getBranchesList(), + variables: [ + "appId": projectId, + "limit": pageSize, + "offset": currentOffset, + "platform": "IOS" + ] + ) + + let newBranches = response.data.app.byId.updateBranches + totalCount = response.data.app.byId.updateBranchesCount + projectName = response.data.app.byId.name + + if currentOffset == 0 { + branches = newBranches + } else { + branches.append(contentsOf: newBranches) + } + + hasMore = branches.count < totalCount + } catch { + self.error = error + self.showingError = true + } + } +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/DevServersSection.swift b/apps/expo-go/ios/Client/SwiftUI/Views/DevServersSection.swift index 7ac12684d620c2..b17fa68448830a 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Views/DevServersSection.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Views/DevServersSection.swift @@ -1,4 +1,5 @@ import SwiftUI +import UIKit struct DevServersSection: View { @EnvironmentObject var viewModel: HomeViewModel @@ -20,7 +21,14 @@ struct DevServersSection: View { if !viewModel.developmentServers.isEmpty { ForEach(viewModel.developmentServers) { server in DevServerRow(server: server) { - viewModel.openApp(url: server.url) + UIImpactFeedbackGenerator(style: .light).impactOccurred() + let normalizedUrl = normalizeDevServerUrl(server.url) + viewModel.addToRecentlyOpened( + url: normalizedUrl, + name: server.description, + iconUrl: server.iconUrl + ) + viewModel.openApp(url: normalizedUrl) } } } @@ -150,4 +158,11 @@ struct DevServersSection: View { troubleshootingMessage = message showingTroubleshootingAlert = true } + + private func normalizeDevServerUrl(_ urlString: String) -> String { + guard let url = URL(string: urlString) else { + return urlString + } + return toExpURLString(url) + } } diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/FeedbackFormView.swift b/apps/expo-go/ios/Client/SwiftUI/Views/FeedbackFormView.swift new file mode 100644 index 00000000000000..17c8f4dff82396 --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Views/FeedbackFormView.swift @@ -0,0 +1,174 @@ +// Copyright ยฉ 2025 650 Industries. All rights reserved. + +import SwiftUI + +struct FeedbackFormView: View { + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var viewModel: HomeViewModel + @State private var feedback = "" + @State private var email = "" + @State private var isSubmitting = false + @State private var submitted = false + @State private var errorMessage: String? + + private let service = FeedbackService() + + var body: some View { + if submitted { + SubmittedView { + dismiss() + } + } else { + FormView(email: $email, feedback: $feedback, errorMessage: $errorMessage, isSubmitting: isSubmitting) { + Task { + await submitFeedback() + } + } + .onAppear { + if email.isEmpty { + email = viewModel.user?.bestContactEmail ?? "" + } + } + } + } + + private func submitFeedback() async { + guard !isSubmitting else { return } + errorMessage = nil + isSubmitting = true + defer { isSubmitting = false } + + let trimmedEmail = email.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedEmail.isEmpty && !isValidEmail(trimmedEmail) { + errorMessage = "Please enter a valid email address." + return + } + + do { + try await service.submitFeedback(message: feedback, email: trimmedEmail.isEmpty ? nil : trimmedEmail) + submitted = true + } catch let error as APIError { + errorMessage = error.localizedDescription + } catch { + errorMessage = error.localizedDescription + } + } + + private func isValidEmail(_ email: String) -> Bool { + if #available(iOS 16, *) { + let regex = /.+@.+\..+/ + return email.wholeMatch(of: regex) != nil + } else { + let fallback = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$" + return NSPredicate(format: "SELF MATCHES %@", fallback).evaluate(with: email) + } + } +} + +struct FormView: View { + @Binding var email: String + @Binding var feedback: String + @Binding var errorMessage: String? + let isSubmitting: Bool + let submitFeedback: () -> Void + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: 12) { + Text("Add your feedback to help us improve the app.") + .font(.subheadline) + .foregroundColor(.secondary) + + Text("Email (optional)") + .font(.headline) + + TextField("your@email.com", text: $email) + .keyboardType(.emailAddress) + .textInputAutocapitalization(.none) + .disableAutocorrection(true) + .padding() + .background(Color.expoSecondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) + + Text("Feedback") + .font(.headline) + + TextEditor(text: $feedback) + .frame(height: 200) + .padding(8) + .background(Color.expoSecondarySystemBackground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) + } + .padding() + } + + if let errorMessage { + VStack(alignment: .leading, spacing: 4) { + Text("Something went wrong. Please try again.") + .font(.subheadline) + .foregroundColor(.red) + Text(errorMessage) + .font(.caption) + .foregroundColor(.red) + } + .padding([.horizontal, .bottom]) + } + + Button { + submitFeedback() + } label: { + if isSubmitting { + ProgressView() + .frame(maxWidth: .infinity) + .padding() + } else { + Text("Submit") + .font(.headline) + .frame(maxWidth: .infinity) + .padding() + } + } + .background(feedback.isEmpty || isSubmitting ? Color.gray.opacity(0.3) : Color.black) + .foregroundColor(feedback.isEmpty || isSubmitting ? .secondary : .white) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + .disabled(feedback.isEmpty || isSubmitting) + .padding() + } + .background(Color.expoSystemBackground) + .navigationTitle("Feedback") + .navigationBarTitleDisplayMode(.inline) + } +} + +struct SubmittedView: View { + let dismiss: () -> Void + + var body: some View { + VStack(spacing: 16) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 64)) + .foregroundColor(.green) + + Text("Thanks for sharing your feedback!") + .font(.headline) + + Text("Your feedback will help us make our app better.") + .font(.subheadline) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + Button("Continue") { + dismiss() + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.black) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + } + .padding() + .navigationTitle("Feedback") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/ProjectDetailsView.swift b/apps/expo-go/ios/Client/SwiftUI/Views/ProjectDetailsView.swift index ad155a7264ba8a..4bdbcb8ee082a3 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Views/ProjectDetailsView.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Views/ProjectDetailsView.swift @@ -6,6 +6,7 @@ struct ProjectDetailsView: View { let projectId: String @StateObject private var viewModel: ProjectDetailsViewModel @EnvironmentObject var homeViewModel: HomeViewModel + @State private var showingAllBranches = false init(projectId: String, initialProject: ExpoProject? = nil) { self.projectId = projectId @@ -34,14 +35,15 @@ struct ProjectDetailsView: View { } else { VStack(spacing: 6) { ForEach(project.updateBranches.prefix(3)) { branch in - BranchRow(branch: branch) { - openBranch(branch) + NavigationLink(destination: BranchDetailsView(projectId: projectId, branchName: branch.name)) { + BranchRowContent(branch: branch) } + .buttonStyle(PlainButtonStyle()) } if project.updateBranches.count > 3 { Button("See all branches (\(project.updateBranches.count))") { - // TODO: Navigate to branches list + showingAllBranches = true } .frame(maxWidth: .infinity) .padding() @@ -70,9 +72,30 @@ struct ProjectDetailsView: View { .background(Color.expoSystemBackground) .navigationTitle(viewModel.project?.name ?? "") .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + if let project = viewModel.project { + showShareSheet(for: project) + } + } label: { + Image(systemName: "square.and.arrow.up") + } + .disabled(viewModel.project == nil) + } + } .task { await viewModel.loadProject() } + .background( + NavigationLink( + destination: BranchesListView(projectId: projectId), + isActive: $showingAllBranches + ) { + EmptyView() + } + .hidden() + ) } @ViewBuilder @@ -87,7 +110,7 @@ struct ProjectDetailsView: View { .foregroundColor(.secondary) if !project.ownerAccount.name.isEmpty { - Text("by \(project.ownerAccount.name)") + Text("Owned by \(project.ownerAccount.name)") .font(.caption) .foregroundColor(.secondary) } @@ -98,18 +121,14 @@ struct ProjectDetailsView: View { .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) } - private func openBranch(_ branch: BranchDetail) { - guard let update = branch.updates.first else { - homeViewModel.showError("This branch has no published updates") - return + private func showShareSheet(for project: ProjectDetail) { + let host = APIClient.shared.apiOrigin.replacingOccurrences(of: "https://", with: "") + let expUrl = "exp://\(host)/\(project.fullName)" + let activityView = UIActivityViewController(activityItems: [expUrl], applicationActivities: nil) + if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let root = scene.windows.first?.rootViewController { + root.present(activityView, animated: true) } - - homeViewModel.openApp(url: update.manifestPermalink) - homeViewModel.addToRecentlyOpened( - url: update.manifestPermalink, - name: "\(viewModel.project?.name ?? "Project") - \(branch.name)", - iconUrl: nil - ) } } @@ -120,7 +139,7 @@ class ProjectDetailsViewModel: ObservableObject { @Published var error: Error? private let projectId: String - private let hasMoreBranches: Bool + private var hasLoadedRemote = false init(projectId: String, initialProject: ExpoProject? = nil) { self.projectId = projectId @@ -136,14 +155,11 @@ class ProjectDetailsViewModel: ObservableObject { BranchDetail(id: branch.id, name: branch.name, updates: branch.updates) } ) - self.hasMoreBranches = initialProject.firstTwoBranches.count == 2 - } else { - self.hasMoreBranches = true } } func loadProject() async { - if project != nil && !hasMoreBranches { + if hasLoadedRemote { return } @@ -160,6 +176,7 @@ class ProjectDetailsViewModel: ObservableObject { ) self.project = response.data.app.byId + self.hasLoadedRemote = true } catch { self.error = error } diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsListView.swift b/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsListView.swift index 0c25912a94316e..f8cc4144cf75b1 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsListView.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsListView.swift @@ -15,6 +15,12 @@ struct ProjectsListView: View { var body: some View { ScrollView { LazyVStack(spacing: 6) { + if viewModel.isLoading && viewModel.projects.isEmpty { + ForEach(0..<3, id: \.self) { _ in + ProjectSkeletonRow() + } + } + ForEach(viewModel.projects) { project in ProjectRowWithNavigation(project: project, shouldNavigateToDetails: true) } @@ -31,7 +37,7 @@ struct ProjectsListView: View { .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) } - if viewModel.isLoading { + if viewModel.isLoading && !viewModel.projects.isEmpty { ProgressView() .padding() } diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsLoadingSection.swift b/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsLoadingSection.swift index deaab665f5f9f2..d5e984ee89b3db 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsLoadingSection.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Views/ProjectsLoadingSection.swift @@ -39,6 +39,5 @@ struct ProjectSkeletonRow: View { .padding() .background(Color.expoSecondarySystemBackground) .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) - .redacted(reason: .placeholder) } } diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/SnacksListView.swift b/apps/expo-go/ios/Client/SwiftUI/Views/SnacksListView.swift index f618dea2a980a8..ae6e30466379eb 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Views/SnacksListView.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Views/SnacksListView.swift @@ -15,6 +15,12 @@ struct SnacksListView: View { var body: some View { ScrollView { LazyVStack(spacing: 6) { + if viewModel.isLoading && viewModel.snacks.isEmpty { + ForEach(0..<3, id: \.self) { _ in + SnackSkeletonRow() + } + } + ForEach(viewModel.snacks) { snack in SnackRowWithAction(snack: snack) } @@ -31,7 +37,7 @@ struct SnacksListView: View { .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) } - if viewModel.isLoading { + if viewModel.isLoading && !viewModel.snacks.isEmpty { ProgressView() .padding() } diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/SnacksLoadingSection.swift b/apps/expo-go/ios/Client/SwiftUI/Views/SnacksLoadingSection.swift index 95bffcf456ed6a..15f6263211250e 100644 --- a/apps/expo-go/ios/Client/SwiftUI/Views/SnacksLoadingSection.swift +++ b/apps/expo-go/ios/Client/SwiftUI/Views/SnacksLoadingSection.swift @@ -39,6 +39,5 @@ struct SnackSkeletonRow: View { .padding() .background(Color.expoSecondarySystemBackground) .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) - .redacted(reason: .placeholder) } } diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/UpgradeWarningView.swift b/apps/expo-go/ios/Client/SwiftUI/Views/UpgradeWarningView.swift new file mode 100644 index 00000000000000..9d50411f4ae157 --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Views/UpgradeWarningView.swift @@ -0,0 +1,185 @@ +// Copyright ยฉ 2025 650 Industries. All rights reserved. + +import SwiftUI + +struct UpgradeWarningView: View { + @State private var shouldShow = false + @State private var betaSdkVersion: String? + @State private var hasDismissedWarning = false + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Group { + if shouldShow && !hasDismissedWarning { + let background = colorScheme == .dark + ? Color(red: 0.22, green: 0.19, blue: 0.08) + : Color(red: 1.0, green: 0.97, blue: 0.84) + let border = colorScheme == .dark + ? Color(red: 0.55, green: 0.45, blue: 0.2) + : Color(red: 0.98, green: 0.8, blue: 0.4) + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .top) { + HStack(spacing: 8) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(Color(red: 0.98, green: 0.7, blue: 0.2)) + Text("New Expo Go version coming soon!") + .font(.system(size: 13, weight: .semibold)) + } + + Spacer() + + Button { + withAnimation(.easeInOut(duration: 0.2)) { + hasDismissedWarning = true + } + } label: { + Image(systemName: "xmark") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.secondary) + .frame(width: 24, height: 24) + } + } + + if let betaSdkVersion { + (Text("A new version of Expo Go will be released to the store soon, and it will ") + .font(.system(size: 13)) + .foregroundColor(.primary) + + Text("only support SDK \(betaSdkVersion).") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.primary) + ) + } + + iosMessage + } + .padding() + .background(background) + .overlay( + RoundedRectangle(cornerRadius: BorderRadius.large) + .stroke(border, lineWidth: 1) + ) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + } + } + .animation(.easeInOut(duration: 0.2), value: hasDismissedWarning) + .task { + await refresh() + } + } + + private var iosMessage: some View { + VStack(alignment: .leading, spacing: 8) { + (Text("In order to ensure that you can upgrade at your own pace, we recommend ") + .font(.system(size: 13)) + .foregroundColor(.primary) + + Text("migrating to a development build.") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.expoBlue) + ) + .onTapGesture { + if let url = URL(string: "https://docs.expo.dev/develop/development-builds/expo-go-to-dev-build") { + UIApplication.shared.open(url) + } + } + (Text("To continue using this version of Expo Go, you can ") + .font(.system(size: 13)) + .foregroundColor(.primary) + + Text("disable automatic app updates") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.primary) + + Text(" from the App Store settings before the new version is released.") + .font(.system(size: 13)) + .foregroundColor(.primary) + ) + } + } + + private func refresh() async { + let result = await UpgradeWarningService.shouldShowUpgradeWarning() + await MainActor.run { + shouldShow = result.shouldShow + betaSdkVersion = result.betaSdkVersion + } + } +} + +private enum UpgradeWarningService { + static func shouldShowUpgradeWarning() async -> (shouldShow: Bool, betaSdkVersion: String?) { + if !isDevice() { + return (false, nil) + } + + guard let url = URL(string: "https://api.expo.dev/v2/versions") else { + return (false, nil) + } + + do { + let (data, response) = try await URLSession.shared.data(from: url) + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) else { + return (false, nil) + } + + let decoder = JSONDecoder() + let versions = try decoder.decode(VersionsResponse.self, from: data) + let published = versions.sdkVersions.compactMap { key, value -> VersionInfo? in + guard let major = extractMajor(from: key) else { return nil } + return VersionInfo(major: major, releaseNoteUrl: value.releaseNoteUrl) + }.sorted(by: { $0.major < $1.major }) + + guard published.count >= 2 else { + return (false, nil) + } + + let last = published[published.count - 1] + let penultimate = published[published.count - 2] + let currentMajor = getSDKMajorVersion(getSupportedSDKVersion()) + let currentIsLatestPublished = currentMajor == String(penultimate.major) + let latestIsBeta = last.releaseNoteUrl == nil + + return (currentIsLatestPublished && latestIsBeta, String(last.major)) + } catch { + return (false, nil) + } + } + + private static func extractMajor(from sdkVersion: String) -> Int? { + if #available(iOS 16, *) { + let pattern = /(\d+)\.0\.0/ + guard let match = sdkVersion.firstMatch(of: pattern), + let major = Int(match.1) else { + return nil + } + return major + } else { + let pattern = #"(\d+)\.0\.0"# + guard let regex = try? NSRegularExpression(pattern: pattern), + let match = regex.firstMatch(in: sdkVersion, range: NSRange(sdkVersion.startIndex..., in: sdkVersion)), + let range = Range(match.range(at: 1), in: sdkVersion) else { + return nil + } + return Int(sdkVersion[range]) + } + } + + private static func isDevice() -> Bool { +#if targetEnvironment(simulator) + return false +#else + return true +#endif + } +} + +private struct VersionsResponse: Decodable { + let sdkVersions: [String: VersionInfoResponse] +} + +private struct VersionInfoResponse: Decodable { + let releaseNoteUrl: String? +} + +private struct VersionInfo { + let major: Int + let releaseNoteUrl: String? +} diff --git a/apps/expo-go/ios/Client/SwiftUI/Views/UserReviewSection.swift b/apps/expo-go/ios/Client/SwiftUI/Views/UserReviewSection.swift new file mode 100644 index 00000000000000..a964c14521e09c --- /dev/null +++ b/apps/expo-go/ios/Client/SwiftUI/Views/UserReviewSection.swift @@ -0,0 +1,73 @@ +// Copyright ยฉ 2025 650 Industries. All rights reserved. + +import SwiftUI + +struct UserReviewSection: View { + @ObservedObject var reviewManager: UserReviewManager + let onProvideFeedback: () -> Void + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + let isDark = colorScheme == .dark + let buttonBackground = isDark ? Color.white : Color.black + let buttonForeground = isDark ? Color.black : Color.white + + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .center) { + Text("Enjoying Expo Go?") + .font(.headline) + + Spacer() + + Button { + reviewManager.dismissReviewSection() + } label: { + Image(systemName: "xmark") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.secondary) + .frame(width: 24, height: 24) + } + } + + Text("Whether you love the app or feel we could be doing better, let us know! Your feedback will help us improve.") + .font(.subheadline) + .foregroundColor(.secondary) + + HStack(spacing: 10) { + Button { + reviewManager.provideFeedback() + onProvideFeedback() + } label: { + Text("Not really") + .font(.subheadline) + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + .background(buttonBackground) + .foregroundColor(buttonForeground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) + } + .buttonStyle(PlainButtonStyle()) + + Button { + reviewManager.requestReview() + } label: { + Text("Love it!") + .font(.subheadline) + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + .background(buttonBackground) + .foregroundColor(buttonForeground) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.medium)) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding() + .background(Color.expoSecondarySystemBackground) + .overlay( + RoundedRectangle(cornerRadius: BorderRadius.large) + .stroke(Color.expoSystemGray4.opacity(0.6), lineWidth: 0.5) + ) + .clipShape(RoundedRectangle(cornerRadius: BorderRadius.large)) + } +} diff --git a/apps/expo-go/ios/Exponent.xcodeproj/project.pbxproj b/apps/expo-go/ios/Exponent.xcodeproj/project.pbxproj index 24b0b59dce69af..c72968fbc5deaa 100644 --- a/apps/expo-go/ios/Exponent.xcodeproj/project.pbxproj +++ b/apps/expo-go/ios/Exponent.xcodeproj/project.pbxproj @@ -8,22 +8,10 @@ /* Begin PBXBuildFile section */ 0349B76C6320565D0B2E851A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 41AAB6FC8982ACA48EAD8CCE /* PrivacyInfo.xcprivacy */; }; - 24C3B781E40BA87F7702303B /* libPods-ExponentIntegrationTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D607CBE135ED3708B0B19DD3 /* libPods-ExponentIntegrationTests.a */; }; 4A62800A2E69DBF000D916B0 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 4A6280092E69DBF000D916B0 /* AppIcon.icon */; }; ABE005EBFA92123617D22216 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D1A518C4DFB7D94147AC4B /* ExpoModulesProvider.swift */; }; B287BB2427DD909C008DA282 /* expo-root.pem in Resources */ = {isa = PBXBuildFile; fileRef = B287BB2327DD909B008DA282 /* expo-root.pem */; }; - B505BA2020CAF80D0046ACFB /* EXEnvironmentMocks.m in Sources */ = {isa = PBXBuildFile; fileRef = B505BA1A20CAF80D0046ACFB /* EXEnvironmentMocks.m */; }; - B505BA2120CAF80D0046ACFB /* EXEnvironmentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B505BA1D20CAF80D0046ACFB /* EXEnvironmentTests.m */; }; - B51333431CE649FE00E9FC9E /* ExponentIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B51333421CE649FE00E9FC9E /* ExponentIntegrationTests.m */; }; - B5854ED720D078C6001D2F6E /* EXClientTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = B5854ED620D078C6001D2F6E /* EXClientTestCase.m */; }; - B5854ED920D0790C001D2F6E /* EXDevTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = B5854ED820D0790C001D2F6E /* EXDevTestCase.m */; }; - B5854EEB20D07D4D001D2F6E /* EXLinkingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B5854EEA20D07D4D001D2F6E /* EXLinkingTests.m */; }; - B5FABDF020DD8E6800642528 /* EXAppLoaderRequestExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = B5FABDEF20DD8E6800642528 /* EXAppLoaderRequestExpectation.m */; }; - B5FABDF220DD91D500642528 /* EXAppLoaderConfigurationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B5FABDF120DD91D500642528 /* EXAppLoaderConfigurationTests.m */; }; - B5FABDF620DD926E00642528 /* EXFileDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B5FABDF520DD926E00642528 /* EXFileDownloaderTests.m */; }; - B5FABDF820DD942A00642528 /* EXAppLoaderRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B5FABDF720DD942A00642528 /* EXAppLoaderRequestTests.m */; }; B832D3668B2FFD1CED2DDE12 /* libPods-Expo Go-Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9003E4BAC9D7FD870EE69703 /* libPods-Expo Go-Tests.a */; }; - E49A0BB45440D0306F0ADCDE /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A29D3F60570DCFE867B5AD19 /* ExpoModulesProvider.swift */; }; EC8D7E182E36030D45964678 /* libPods-Expo Go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D8ABD4D495DA97B3FFF6F8A3 /* libPods-Expo Go.a */; }; F12A1AB922ABD4BA008542C6 /* generate-dynamic-macros.sh in Resources */ = {isa = PBXBuildFile; fileRef = F12A1AB822ABD4BA008542C6 /* generate-dynamic-macros.sh */; }; F77DDB931E04AC1100624CA2 /* SafariServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F77DDB921E04AC1100624CA2 /* SafariServices.framework */; }; @@ -40,13 +28,6 @@ remoteGlobalIDString = 78CEE2BF1ACD07D70095B124; remoteInfo = Exponent; }; - B51333451CE649FE00E9FC9E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 78CEE2B81ACD07D70095B124 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 78CEE2BF1ACD07D70095B124; - remoteInfo = Exponent; - }; FCF6D37225B34BC200808BF5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 78CEE2B81ACD07D70095B124 /* Project object */; @@ -86,29 +67,9 @@ 88A25B1DAF71DFE5C4134A46 /* libPods-Expo Go-Expo Go (versioned).a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Expo Go-Expo Go (versioned).a"; sourceTree = BUILT_PRODUCTS_DIR; }; 8EE60BD85240AF63736C047A /* Pods-Expo Go-Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Expo Go-Tests.debug.xcconfig"; path = "Target Support Files/Pods-Expo Go-Tests/Pods-Expo Go-Tests.debug.xcconfig"; sourceTree = ""; }; 9003E4BAC9D7FD870EE69703 /* libPods-Expo Go-Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Expo Go-Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - A29D3F60570DCFE867B5AD19 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-ExponentIntegrationTests/ExpoModulesProvider.swift"; sourceTree = ""; }; A6EAF60D92D03B190DFCD5D2 /* Pods-Expo Go-Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Expo Go-Tests.release.xcconfig"; path = "Target Support Files/Pods-Expo Go-Tests/Pods-Expo Go-Tests.release.xcconfig"; sourceTree = ""; }; B287BB2327DD909B008DA282 /* expo-root.pem */ = {isa = PBXFileReference; lastKnownFileType = text; name = "expo-root.pem"; path = "Exponent/Supporting/expo-root.pem"; sourceTree = ""; }; B505BA0E20CAF77A0046ACFB /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B505BA1220CAF77A0046ACFB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B505BA1A20CAF80D0046ACFB /* EXEnvironmentMocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXEnvironmentMocks.m; sourceTree = ""; }; - B505BA1B20CAF80D0046ACFB /* EXEnvironment+Tests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "EXEnvironment+Tests.h"; sourceTree = ""; }; - B505BA1C20CAF80D0046ACFB /* EXEnvironmentMocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EXEnvironmentMocks.h; sourceTree = ""; }; - B505BA1D20CAF80D0046ACFB /* EXEnvironmentTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXEnvironmentTests.m; sourceTree = ""; }; - B51333401CE649FE00E9FC9E /* ExponentIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExponentIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B51333421CE649FE00E9FC9E /* ExponentIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExponentIntegrationTests.m; sourceTree = ""; }; - B51333441CE649FE00E9FC9E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B5854ED620D078C6001D2F6E /* EXClientTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EXClientTestCase.m; sourceTree = ""; }; - B5854ED820D0790C001D2F6E /* EXDevTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EXDevTestCase.m; sourceTree = ""; }; - B5854EE420D07AE3001D2F6E /* EXClientTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EXClientTestCase.h; sourceTree = ""; }; - B5854EE520D07B3E001D2F6E /* EXDevTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EXDevTestCase.h; sourceTree = ""; }; - B5854EE720D07BCB001D2F6E /* EXAppLoader+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "EXAppLoader+Tests.h"; sourceTree = ""; }; - B5854EEA20D07D4D001D2F6E /* EXLinkingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXLinkingTests.m; sourceTree = ""; }; - B5FABDEE20DD8E6800642528 /* EXAppLoaderRequestExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EXAppLoaderRequestExpectation.h; sourceTree = ""; }; - B5FABDEF20DD8E6800642528 /* EXAppLoaderRequestExpectation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXAppLoaderRequestExpectation.m; sourceTree = ""; }; - B5FABDF120DD91D500642528 /* EXAppLoaderConfigurationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXAppLoaderConfigurationTests.m; sourceTree = ""; }; - B5FABDF520DD926E00642528 /* EXFileDownloaderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EXFileDownloaderTests.m; sourceTree = ""; }; - B5FABDF720DD942A00642528 /* EXAppLoaderRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXAppLoaderRequestTests.m; sourceTree = ""; }; C279B31A23EF096F3969DAD5 /* Pods-Expo Go.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Expo Go.debug.xcconfig"; path = "Target Support Files/Pods-Expo Go/Pods-Expo Go.debug.xcconfig"; sourceTree = ""; }; D607CBE135ED3708B0B19DD3 /* libPods-ExponentIntegrationTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ExponentIntegrationTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; D8ABD4D495DA97B3FFF6F8A3 /* libPods-Expo Go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Expo Go.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -176,14 +137,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B513333D1CE649FE00E9FC9E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 24C3B781E40BA87F7702303B /* libPods-ExponentIntegrationTests.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; FCF6D36A25B34BC200808BF5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -214,14 +167,6 @@ name = Frameworks; sourceTree = ""; }; - 78B070920855541A4D469D4D /* ExponentIntegrationTests */ = { - isa = PBXGroup; - children = ( - A29D3F60570DCFE867B5AD19 /* ExpoModulesProvider.swift */, - ); - name = ExponentIntegrationTests; - sourceTree = ""; - }; 78CEE2B71ACD07D70095B124 = { isa = PBXGroup; children = ( @@ -231,12 +176,10 @@ XXEXPOCLIENTXXXXXXXXXXXX /* Client */, XXEXPONENTXXXXXXXXXXXXXX /* Exponent */, F1F7433C24ABECD400EA5023 /* Exponent-Bridging-Header.h */, - B51333411CE649FE00E9FC9E /* ExponentIntegrationTests */, FCF6D36E25B34BC200808BF5 /* ExpoNotificationServiceExtension */, 3050F5A98E56E9379C36ABD2 /* Frameworks */, AE58247EE2419C33B3A8CBAE /* Pods */, 78CEE2C11ACD07D70095B124 /* Products */, - B505BA0F20CAF77A0046ACFB /* Tests */, 994130004C859A4758AD583C /* ExpoModulesProviders */, 41AAB6FC8982ACA48EAD8CCE /* PrivacyInfo.xcprivacy */, ); @@ -248,7 +191,6 @@ isa = PBXGroup; children = ( 78CEE2C01ACD07D70095B124 /* Expo Go.app */, - B51333401CE649FE00E9FC9E /* ExponentIntegrationTests.xctest */, B505BA0E20CAF77A0046ACFB /* Tests.xctest */, FCF6D36D25B34BC200808BF5 /* ExpoNotificationServiceExtension.appex */, ); @@ -260,7 +202,6 @@ children = ( CB1F0CEE33FA52600597D003 /* Expo Go */, 2676C57A9176C75127D22133 /* Tests */, - 78B070920855541A4D469D4D /* ExponentIntegrationTests */, ); name = ExpoModulesProviders; sourceTree = ""; @@ -282,62 +223,6 @@ path = Pods; sourceTree = ""; }; - B505BA0F20CAF77A0046ACFB /* Tests */ = { - isa = PBXGroup; - children = ( - B5854EDE20D07996001D2F6E /* AppLoader */, - B505BA1920CAF80D0046ACFB /* Environment */, - B5854EDC20D07996001D2F6E /* Linking */, - B505BA1220CAF77A0046ACFB /* Info.plist */, - ); - path = Tests; - sourceTree = ""; - }; - B505BA1920CAF80D0046ACFB /* Environment */ = { - isa = PBXGroup; - children = ( - B505BA1B20CAF80D0046ACFB /* EXEnvironment+Tests.h */, - B505BA1D20CAF80D0046ACFB /* EXEnvironmentTests.m */, - B505BA1C20CAF80D0046ACFB /* EXEnvironmentMocks.h */, - B505BA1A20CAF80D0046ACFB /* EXEnvironmentMocks.m */, - B5854EE420D07AE3001D2F6E /* EXClientTestCase.h */, - B5854ED620D078C6001D2F6E /* EXClientTestCase.m */, - B5854EE520D07B3E001D2F6E /* EXDevTestCase.h */, - B5854ED820D0790C001D2F6E /* EXDevTestCase.m */, - ); - path = Environment; - sourceTree = ""; - }; - B51333411CE649FE00E9FC9E /* ExponentIntegrationTests */ = { - isa = PBXGroup; - children = ( - B51333421CE649FE00E9FC9E /* ExponentIntegrationTests.m */, - B51333441CE649FE00E9FC9E /* Info.plist */, - ); - path = ExponentIntegrationTests; - sourceTree = ""; - }; - B5854EDC20D07996001D2F6E /* Linking */ = { - isa = PBXGroup; - children = ( - B5854EEA20D07D4D001D2F6E /* EXLinkingTests.m */, - ); - path = Linking; - sourceTree = ""; - }; - B5854EDE20D07996001D2F6E /* AppLoader */ = { - isa = PBXGroup; - children = ( - B5854EE720D07BCB001D2F6E /* EXAppLoader+Tests.h */, - B5FABDF120DD91D500642528 /* EXAppLoaderConfigurationTests.m */, - B5FABDEE20DD8E6800642528 /* EXAppLoaderRequestExpectation.h */, - B5FABDEF20DD8E6800642528 /* EXAppLoaderRequestExpectation.m */, - B5FABDF720DD942A00642528 /* EXAppLoaderRequestTests.m */, - B5FABDF520DD926E00642528 /* EXFileDownloaderTests.m */, - ); - path = AppLoader; - sourceTree = ""; - }; CB1F0CEE33FA52600597D003 /* Expo Go */ = { isa = PBXGroup; children = ( @@ -420,26 +305,6 @@ productReference = B505BA0E20CAF77A0046ACFB /* Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - B513333F1CE649FE00E9FC9E /* ExponentIntegrationTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = B513334A1CE649FE00E9FC9E /* Build configuration list for PBXNativeTarget "ExponentIntegrationTests" */; - buildPhases = ( - 2DA4AA338E6D5422567DE1EB /* [CP] Check Pods Manifest.lock */, - A04F3C9AC66B709E16F4F5AB /* [Expo] Configure project */, - B513333C1CE649FE00E9FC9E /* Sources */, - B513333D1CE649FE00E9FC9E /* Frameworks */, - B513333E1CE649FE00E9FC9E /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - B51333461CE649FE00E9FC9E /* PBXTargetDependency */, - ); - name = ExponentIntegrationTests; - productName = ExponentIntegrationTests; - productReference = B51333401CE649FE00E9FC9E /* ExponentIntegrationTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; FCF6D36C25B34BC200808BF5 /* ExpoNotificationServiceExtension */ = { isa = PBXNativeTarget; buildConfigurationList = FCF6D37825B34BC200808BF5 /* Build configuration list for PBXNativeTarget "ExpoNotificationServiceExtension" */; @@ -498,10 +363,6 @@ ProvisioningStyle = Automatic; TestTargetID = 78CEE2BF1ACD07D70095B124; }; - B513333F1CE649FE00E9FC9E = { - CreatedOnToolsVersion = 7.3.1; - TestTargetID = 78CEE2BF1ACD07D70095B124; - }; FCF6D36C25B34BC200808BF5 = { CreatedOnToolsVersion = 12.2; DevelopmentTeam = C8D8QTF339; @@ -525,7 +386,6 @@ projectRoot = ""; targets = ( 78CEE2BF1ACD07D70095B124 /* Expo Go */, - B513333F1CE649FE00E9FC9E /* ExponentIntegrationTests */, B505BA0D20CAF77A0046ACFB /* Tests */, FCF6D36C25B34BC200808BF5 /* ExpoNotificationServiceExtension */, ); @@ -551,13 +411,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B513333E1CE649FE00E9FC9E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; FCF6D36B25B34BC200808BF5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -568,28 +421,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 2DA4AA338E6D5422567DE1EB /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ExponentIntegrationTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 347794D3234BFFC20087B402 /* Copy Bundle Resources Conditionally */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -680,29 +511,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Expo Go/Pods-Expo Go-resources.sh\"\n"; showEnvVarsInLog = 0; }; - A04F3C9AC66B709E16F4F5AB /* [Expo] Configure project */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/.xcode.env", - "$(SRCROOT)/.xcode.env.local", - "$(SRCROOT)/Pods/Target Support Files/Pods-ExponentIntegrationTests/expo-configure-project.sh", - ); - name = "[Expo] Configure project"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(SRCROOT)/Pods/Target Support Files/Pods-ExponentIntegrationTests/ExpoModulesProvider.swift", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-ExponentIntegrationTests/expo-configure-project.sh\"\n"; - }; B2C4E74600B5875B0118890B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -843,28 +651,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B5FABDF020DD8E6800642528 /* EXAppLoaderRequestExpectation.m in Sources */, - B505BA2020CAF80D0046ACFB /* EXEnvironmentMocks.m in Sources */, - B5FABDF820DD942A00642528 /* EXAppLoaderRequestTests.m in Sources */, - B5854ED720D078C6001D2F6E /* EXClientTestCase.m in Sources */, - B505BA2120CAF80D0046ACFB /* EXEnvironmentTests.m in Sources */, - B5FABDF620DD926E00642528 /* EXFileDownloaderTests.m in Sources */, - B5FABDF220DD91D500642528 /* EXAppLoaderConfigurationTests.m in Sources */, - B5854ED920D0790C001D2F6E /* EXDevTestCase.m in Sources */, - B5854EEB20D07D4D001D2F6E /* EXLinkingTests.m in Sources */, FB4B49E32E5D99D79400A124 /* ExpoModulesProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - B513333C1CE649FE00E9FC9E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B51333431CE649FE00E9FC9E /* ExponentIntegrationTests.m in Sources */, - E49A0BB45440D0306F0ADCDE /* ExpoModulesProvider.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; FCF6D36925B34BC200808BF5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -881,11 +671,6 @@ target = 78CEE2BF1ACD07D70095B124 /* Expo Go */; targetProxy = B505BA1320CAF77A0046ACFB /* PBXContainerItemProxy */; }; - B51333461CE649FE00E9FC9E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 78CEE2BF1ACD07D70095B124 /* Expo Go */; - targetProxy = B51333451CE649FE00E9FC9E /* PBXContainerItemProxy */; - }; FCF6D37325B34BC200808BF5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = FCF6D36C25B34BC200808BF5 /* ExpoNotificationServiceExtension */; @@ -1188,7 +973,6 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1198,7 +982,7 @@ DEVELOPMENT_TEAM = C8D8QTF339; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1209,6 +993,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Expo Go.app/Exponent"; + TVOS_DEPLOYMENT_TARGET = 15.1; }; name = Debug; }; @@ -1220,7 +1005,6 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -1230,7 +1014,7 @@ DEVELOPMENT_TEAM = C8D8QTF339; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1241,52 +1025,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Expo Go.app/Exponent"; - }; - name = Release; - }; - B51333471CE649FE00E9FC9E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 1A7952F30F72271C821848D7 /* Pods-ExponentIntegrationTests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NONNULL = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - GCC_NO_COMMON_BLOCKS = YES; - INFOPLIST_FILE = ExponentIntegrationTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = host.exp.ExponentIntegrationTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Expo Go.app/Exponent"; - }; - name = Debug; - }; - B51333481CE649FE00E9FC9E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 39E8C61B17879D7B4A44D983 /* Pods-ExponentIntegrationTests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NONNULL = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - GCC_NO_COMMON_BLOCKS = YES; - INFOPLIST_FILE = ExponentIntegrationTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = host.exp.ExponentIntegrationTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Expo Go.app/Exponent"; + TVOS_DEPLOYMENT_TARGET = 15.1; }; name = Release; }; @@ -1400,15 +1139,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - B513334A1CE649FE00E9FC9E /* Build configuration list for PBXNativeTarget "ExponentIntegrationTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B51333471CE649FE00E9FC9E /* Debug */, - B51333481CE649FE00E9FC9E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; FCF6D37825B34BC200808BF5 /* Build configuration list for PBXNativeTarget "ExpoNotificationServiceExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m b/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m index cfba0629f48b2d..cdd46009da96da 100644 --- a/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m +++ b/apps/expo-go/ios/Exponent/ExpoKit/ExpoKit.m @@ -11,7 +11,7 @@ #import -#import + @interface ExpoKit () @@ -82,17 +82,6 @@ - (void)prepareWithLaunchOptions:(nullable NSDictionary *)launchOptions [DDLog addLogger:[DDOSLogger sharedInstance]]; RCTSetFatalHandler(handleFatalReactError); - NSString *standaloneGMSKey = [[NSBundle mainBundle].infoDictionary objectForKey:@"GMSApiKey"]; - if (standaloneGMSKey && standaloneGMSKey.length) { - [GMSServices provideAPIKey:standaloneGMSKey]; - } else { - if (_applicationKeys[@"GOOGLE_MAPS_IOS_API_KEY"]) {// we may define this as empty - if ([_applicationKeys[@"GOOGLE_MAPS_IOS_API_KEY"] length]) { - [GMSServices provideAPIKey:_applicationKeys[@"GOOGLE_MAPS_IOS_API_KEY"]]; - } - } - } - _launchOptions = launchOptions; } diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/Contents.json b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/Contents.json new file mode 100644 index 00000000000000..d1271aac9d5771 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "branch-icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "branch-icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "branch-icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon.png b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon.png new file mode 100644 index 00000000000000..e1262ab67562e2 Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon@2x.png b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon@2x.png new file mode 100644 index 00000000000000..fbd10a28ac9630 Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon@2x.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon@3x.png b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon@3x.png new file mode 100644 index 00000000000000..c479ee6a94be87 Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/branch-icon.imageset/branch-icon@3x.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/cli.imageset/Contents.json b/apps/expo-go/ios/Exponent/Images.xcassets/cli.imageset/Contents.json new file mode 100644 index 00000000000000..d735ed3a3b5632 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Images.xcassets/cli.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cli.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/cli.imageset/cli.png b/apps/expo-go/ios/Exponent/Images.xcassets/cli.imageset/cli.png new file mode 100644 index 00000000000000..f922b85a65904d Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/cli.imageset/cli.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/snack.imageset/Contents.json b/apps/expo-go/ios/Exponent/Images.xcassets/snack.imageset/Contents.json new file mode 100644 index 00000000000000..a2b772138bb9ff --- /dev/null +++ b/apps/expo-go/ios/Exponent/Images.xcassets/snack.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "snack.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/snack.imageset/snack.png b/apps/expo-go/ios/Exponent/Images.xcassets/snack.imageset/snack.png new file mode 100644 index 00000000000000..bc3ef4b5f024d9 Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/snack.imageset/snack.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/Contents.json b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/Contents.json new file mode 100644 index 00000000000000..567c72b1a8d1d3 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "update-icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "update-icon-light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "update-icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "update-icon-light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "update-icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "update-icon-light@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light.png b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light.png new file mode 100644 index 00000000000000..b5adc1ec3dc5a0 Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light@2x.png b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light@2x.png new file mode 100644 index 00000000000000..1daa9741267baf Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light@2x.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light@3x.png b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light@3x.png new file mode 100644 index 00000000000000..a60ca3dd117642 Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon-light@3x.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon.png b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon.png new file mode 100644 index 00000000000000..42e135863c582b Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon@2x.png b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon@2x.png new file mode 100644 index 00000000000000..4e2e83f777b6ca Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon@2x.png differ diff --git a/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon@3x.png b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon@3x.png new file mode 100644 index 00000000000000..2ab13a4e3bbbe3 Binary files /dev/null and b/apps/expo-go/ios/Exponent/Images.xcassets/update-icon.imageset/update-icon@3x.png differ diff --git a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m index c00f3856943c3a..1f54a2c7823c32 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m +++ b/apps/expo-go/ios/Exponent/Kernel/Core/EXKernel.m @@ -300,6 +300,14 @@ - (void)devMenuNavigateHome { [self switchTasks]; } +- (void)devMenuTogglePerformanceMonitor { + [[self visibleApp].appManager togglePerformanceMonitor]; +} + +- (void)devMenuToggleElementInspector { + [[self visibleApp].appManager toggleElementInspector]; +} + @end NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm index 16f5de04278b9a..c1f63332232b84 100644 --- a/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm +++ b/apps/expo-go/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm @@ -422,7 +422,7 @@ - (BOOL)enablesDeveloperTools { EXManifestsManifest *manifest = _appRecord.appLoader.manifest; if (manifest) { - return manifest.isUsingDeveloperTool; + return manifest.isUsingDeveloperTool || manifest.isDevelopmentMode; } return false; } @@ -464,7 +464,7 @@ - (void)toggleElementInspector - (void)toggleDevMenu { - [[EXKernel sharedInstance] switchTasks]; + [self showDevMenu]; } - (void)setupWebSocketControls diff --git a/apps/expo-go/ios/Exponent/Kernel/Services/EXSplashScreen/EXSplashScreenService.m b/apps/expo-go/ios/Exponent/Kernel/Services/EXSplashScreen/EXSplashScreenService.m index 8f509492876a9e..44c7a43596b56d 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Services/EXSplashScreen/EXSplashScreenService.m +++ b/apps/expo-go/ios/Exponent/Kernel/Services/EXSplashScreen/EXSplashScreenService.m @@ -43,7 +43,7 @@ - (void)showSplashScreenFor:(UIViewController *)viewController options:options splashScreenViewProvider:splashScreenViewProvider successCallback:^{} - failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }]; + failureCallback:^(NSString *message){ RCTLogWarn(@"%@", message); }]; } - (void)showSplashScreenFor:(UIViewController *)viewController diff --git a/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm b/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm index e0d74d1a900d18..1e799d6298ec1e 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm +++ b/apps/expo-go/ios/Exponent/Kernel/Views/EXAppViewController.mm @@ -57,7 +57,6 @@ @interface EXAppViewController () @property (nonatomic, strong) NSDate *dtmLastFatalErrorShown; @property (nonatomic, strong) NSMutableArray *backgroundedControllers; -@property (nonatomic, assign) BOOL isHomeApp; @property (nonatomic, assign) UIInterfaceOrientation previousInterfaceOrientation; /* @@ -110,9 +109,7 @@ - (void)dealloc - (void)viewDidLoad { [super viewDidLoad]; - _isHomeApp = NO; - self.appLoadingCancelView = [EXAppLoadingCancelView new]; self.appLoadingCancelView.delegate = self; [self.view addSubview:self.appLoadingCancelView]; @@ -237,15 +234,12 @@ - (bool)_readForcesRTLFromManifest:(EXManifestsManifest *)manifest - (void)appStateDidBecomeActive { - if (_isHomeApp) { - [EXTextDirectionController setRTLPreferences:false :false]; - } else if(_appRecord.appLoader.manifest != nil) { - BOOL supportsRTL = [self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]; - BOOL forceRTL = [self _readForcesRTLFromManifest:_appRecord.appLoader.manifest]; - [EXTextDirectionController setRTLPreferences:supportsRTL :forceRTL]; + if (_appRecord.appLoader.manifest != nil) { + BOOL supportsRTL = [self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]; + BOOL forceRTL = [self _readForcesRTLFromManifest:_appRecord.appLoader.manifest]; + [EXTextDirectionController setRTLPreferences:supportsRTL :forceRTL]; } dispatch_async(dispatch_get_main_queue(), ^{ - // Reset the root view background color and window color if we switch between Expo home and project [self _setBackgroundColor]; }); } @@ -303,13 +297,9 @@ - (void)backgroundControllers * - actual one served when app is fetched. * For each of them we should show SplashScreen, * therefore for any consecutive SplashScreen.show call we just reconfigure what's already visible. - * In HomeApp or standalone apps this function is no-op as SplashScreen is managed differently. */ - (void)_showOrReconfigureManagedAppSplashScreen:(EXManifestsManifest *)manifest { - if (_isHomeApp) { - return; - } if (!_managedAppSplashScreenViewProvider) { _managedAppSplashScreenViewProvider = [[EXManagedAppSplashScreenViewProvider alloc] initWith:manifest]; @@ -321,10 +311,6 @@ - (void)_showOrReconfigureManagedAppSplashScreen:(EXManifestsManifest *)manifest - (void)_showCachedExperienceAlert { - if (self.isHomeApp) { - return; - } - dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Using a cached project" @@ -368,7 +354,7 @@ - (void)_showSplashScreenWithProvider:(id)provider options:EXSplashScreenDefault successCallback:^(BOOL hasEffect){} failureCallback:^(NSString * _Nonnull message) { - EXLogWarn(@"Hiding splash screen from root view controller did not succeed: %@", message); + RCTLogWarn(@"Hiding splash screen from root view controller did not succeed: %@", message); }]; }); }; @@ -380,7 +366,7 @@ - (void)_showSplashScreenWithProvider:(id)provider options:EXSplashScreenDefault splashScreenViewProvider:provider successCallback:hideRootViewControllerSplashScreen - failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }]; + failureCallback:^(NSString *message){ RCTLogWarn(@"%@", message); }]; }); } @@ -400,7 +386,7 @@ - (void)_showManagedSplashScreenWithProvider:(id)pro options:EXSplashScreenDefault splashScreenController:self.managedSplashScreenController successCallback:^{} - failureCallback:^(NSString *message){ EXLogWarn(@"%@", message); }]; + failureCallback:^(NSString *message){ RCTLogWarn(@"%@", message); }]; }); } @@ -443,11 +429,9 @@ - (void)appLoader:(EXAbstractLoader *)appLoader didLoadBundleWithProgress:(EXLoa - (void)appLoader:(EXAbstractLoader *)appLoader didFinishLoadingManifest:(EXManifestsManifest *)manifest bundle:(NSData *)data { [self _showOrReconfigureManagedAppSplashScreen:manifest]; - if (!_isHomeApp) { - BOOL supportsRTL = [self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]; - BOOL forceRTL = [self _readForcesRTLFromManifest:_appRecord.appLoader.manifest]; - [EXTextDirectionController setRTLPreferences:supportsRTL :forceRTL]; - } + BOOL supportsRTL = [self _readSupportsRTLFromManifest:_appRecord.appLoader.manifest]; + BOOL forceRTL = [self _readForcesRTLFromManifest:_appRecord.appLoader.manifest]; + [EXTextDirectionController setRTLPreferences:supportsRTL :forceRTL]; [self _rebuildHost]; if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { [self->_appRecord.appManager appLoaderFinished]; @@ -543,7 +527,7 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations return [super supportedInterfaceOrientations]; } - if ([ScreenOrientationRegistry.shared requiredOrientationMask] > 0 && !self.isHomeApp) { + if ([ScreenOrientationRegistry.shared requiredOrientationMask] > 0) { return [ScreenOrientationRegistry.shared requiredOrientationMask]; } diff --git a/apps/expo-go/ios/Exponent/Kernel/Views/Loading/EXAppLoadingProgressWindowController.m b/apps/expo-go/ios/Exponent/Kernel/Views/Loading/EXAppLoadingProgressWindowController.m index 132ecba18bfaa5..e787ff0caec525 100644 --- a/apps/expo-go/ios/Exponent/Kernel/Views/Loading/EXAppLoadingProgressWindowController.m +++ b/apps/expo-go/ios/Exponent/Kernel/Views/Loading/EXAppLoadingProgressWindowController.m @@ -38,7 +38,7 @@ - (void)show if (!self.window) { CGSize screenSize = [UIScreen mainScreen].bounds.size; - int bottomInsets = EXSharedApplication().keyWindow.safeAreaInsets.bottom; + int bottomInsets = RCTSharedApplication().keyWindow.safeAreaInsets.bottom; self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, screenSize.height - 36 - bottomInsets, screenSize.width, diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.h b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.h index 0a10556e9312be..8088b085ed403a 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.h +++ b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.h @@ -4,6 +4,7 @@ #import @class RCTHost; +@class EXAppContext; NS_ASSUME_NONNULL_BEGIN @@ -18,6 +19,11 @@ typedef void (^OnLoad)(NSURL *sourceURL, RCTSourceLoadBlock loadCallback); - (instancetype)initWithSourceURL:(NSURL *)sourceURL manager:(EXVersionManagerObjC *)manager onLoad:(OnLoad)onLoad; +/** + * Creates a new app context configured for Expo Go. + */ +- (EXAppContext *)createExpoGoAppContext; + @end NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.mm b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.mm index 16492cc50484d9..93c69876add068 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.mm +++ b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoAppInstance.mm @@ -50,4 +50,8 @@ - (void)loadBundleAtURL:(NSURL *)sourceURL - (void)host:(nonnull RCTHost *)host didReceiveJSErrorStack:(nonnull NSArray *> *)stack message:(nonnull NSString *)message exceptionId:(NSUInteger)exceptionId isFatal:(BOOL)isFatal { } +- (EXAppContext *)createExpoGoAppContext { + return [_manager createExpoGoAppContext]; +} + @end diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.h b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.h index 89465cb90c880d..cf16f52f7c8fad 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.h +++ b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.h @@ -1,5 +1,9 @@ +#import #import -@interface ExpoGoReactNativeFactory : RCTReactNativeFactory +@protocol RCTHostDelegate; +@protocol RCTHostRuntimeDelegate; + +@interface ExpoGoReactNativeFactory : RCTReactNativeFactory @end diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.mm b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.mm index e8f80122e97c12..7ce5048c74c735 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.mm +++ b/apps/expo-go/ios/Exponent/Versioned/Core/AppInstance/ExpoGoReactNativeFactory.mm @@ -1,7 +1,11 @@ #import "ExpoGoReactNativeFactory.h" +#import "ExpoAppInstance.h" #import #import +#import +#import + @implementation ExpoGoReactNativeFactory @@ -34,4 +38,23 @@ - (void)loadBundleAtURL:(NSURL *)sourceURL onProgress:(RCTSourceLoadProgressBloc } } +- (void)hostDidStart:(nonnull RCTHost *)host { + host.runtimeDelegate = self; + if ([self.delegate respondsToSelector:@selector(hostDidStart:)]) { + [self.delegate hostDidStart:host]; + } +} + +- (void)host:(nonnull RCTHost *)host didInitializeRuntime:(jsi::Runtime &)runtime +{ + ExpoAppInstance *appInstance = (ExpoAppInstance *)self.delegate; + EXAppContext *appContext = [appInstance createExpoGoAppContext]; + + // Inject and decorate the `global.expo` object + appContext._runtime = [[EXRuntime alloc] initWithRuntime:runtime]; + [appContext setHostWrapper:[[EXHostWrapper alloc] initWithHost:host]]; + + [appContext registerNativeModules]; +} + @end diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.h b/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.h index 7ad6ca0d6246c1..1bbe616195813e 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.h +++ b/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.h @@ -4,8 +4,8 @@ #import #import - @class EXManifestsManifest; +@class EXAppContext; @interface EXVersionManagerObjC : NSObject @@ -40,4 +40,9 @@ - (Class)getModuleClassFromName:(const char *)name; +/** + * Creates a new app context configured for Expo Go. + */ +- (nonnull EXAppContext *)createExpoGoAppContext; + @end diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.mm b/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.mm index ba661986dbdcb8..e9eebbd7acaa9a 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.mm +++ b/apps/expo-go/ios/Exponent/Versioned/Core/EXVersionManagerObjC.mm @@ -3,11 +3,14 @@ #import "EXAppState.h" #import "EXDevSettings.h" #import "EXDisabledDevLoadingView.h" +#import "EXExpoPerfMonitor.h" #import "EXDisabledRedBox.h" #import "EXVersionManagerObjC.h" #import "EXStatusBarManager.h" #import "EXTest.h" +#import + #import #import #import @@ -261,7 +264,7 @@ - (uint32_t)addWebSocketNotificationHandler:(void (^)(NSDictionary)_moduleInstanceForHost:(id)host named:(NSString *)name { - return [[host moduleRegistry] moduleForName:[name UTF8String]]; + const char *cName = [name UTF8String]; + id module = [[host moduleRegistry] moduleForName:cName]; + if (module && strcmp(cName, "PerfMonitor") == 0 && [module respondsToSelector:@selector(updateHost:)]) { + [module updateHost:host]; + } + return module; } - (NSArray *)extraModules @@ -355,6 +363,9 @@ - (Class)getModuleClassFromName:(const char *)name if (strcmp(name, "DevSettings") == 0) { return EXDevSettings.class; } + if (strcmp(name, "PerfMonitor") == 0) { + return EXExpoPerfMonitor.class; + } if (strcmp(name, "RedBox") == 0) { if (![_params[@"isDeveloper"] boolValue]) { // user-facing (not debugging). diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXExpoPerfMonitor.h b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXExpoPerfMonitor.h new file mode 100644 index 00000000000000..06a1605caf21d0 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXExpoPerfMonitor.h @@ -0,0 +1,15 @@ +#import + +#import +#import +#import +#import +#import "EXPerfMonitorDataSource.h" + +@interface EXExpoPerfMonitor : NSObject + +- (void)show; +- (void)hide; +- (void)updateHost:(nullable RCTHost *)host; + +@end diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXExpoPerfMonitor.mm b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXExpoPerfMonitor.mm new file mode 100644 index 00000000000000..371eff6cd45243 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXExpoPerfMonitor.mm @@ -0,0 +1,241 @@ +#import "EXExpoPerfMonitor.h" + +#import +#import +#import +#import +#import +#import "Expo_Go-Swift.h" + +static const CGFloat EXPerfMonitorDefaultWidth = 260; +static const CGFloat EXPerfMonitorMinimumHeight = 176; +static const CGFloat EXPerfMonitorMaxWidth = 360.0; +static const CGFloat EXPerfMonitorScreenWidthRatio = 0.95; +static const CGFloat EXPerfMonitorTopMargin = 12.0; + +static CGFloat EXPerfMonitorTargetWidthForWindow(UIWindow *window) +{ + if (!window) { + return EXPerfMonitorDefaultWidth; + } + CGFloat desiredWidth = window.bounds.size.width * EXPerfMonitorScreenWidthRatio; + CGFloat clampedWidth = MIN(desiredWidth, EXPerfMonitorMaxWidth); + return MAX(EXPerfMonitorDefaultWidth, clampedWidth); +} + +@interface EXExpoPerfMonitor () + +@property (nonatomic, strong) EXPerfMonitorDataSource *dataSource; +@property (nonatomic, strong) EXPerfMonitorPresenter *presenter; +@property (nonatomic, strong) UIView *container; +@property (nonatomic, weak) RCTHost *currentHost; + +@end + +@implementation EXExpoPerfMonitor { + __weak RCTBridge *_bridge; + __weak RCTModuleRegistry *_moduleRegistry; +} + +@synthesize bridge = _bridge; +@synthesize moduleRegistry = _moduleRegistry; + +RCT_EXPORT_MODULE() + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +- (void)invalidate +{ + [self hide]; +} + +- (void)updateHost:(RCTHost *)host +{ + self.currentHost = host; + if (self.dataSource) { + self.dataSource.host = host; + } +} + +- (void)handlePan:(UIPanGestureRecognizer *)gesture +{ + UIView *superview = self.container.superview; + if (!superview) { + return; + } + + CGPoint translation = [gesture translationInView:superview]; + self.container.center = CGPointMake(self.container.center.x + translation.x, + self.container.center.y + translation.y); + [gesture setTranslation:CGPointZero inView:superview]; +} + +- (void)show +{ + if (!_bridge) { + return; + } + + if (!self.presenter) { + self.presenter = [[EXPerfMonitorPresenter alloc] init]; + + __weak __typeof(self) weakSelf = self; + [self.presenter setContentSizeHandler:^(NSValue *value) { + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateContainerForContentSize:value.CGSizeValue]; + }); + }]; + + [self.presenter setCloseHandler:^{ + [weakSelf hide]; + }]; + } + + if (!self.container) { + self.container = [[UIView alloc] initWithFrame:CGRectZero]; + self.container.backgroundColor = UIColor.clearColor; + self.container.layer.masksToBounds = NO; + + UIView *hostView = self.presenter.view; + hostView.frame = self.container.bounds; + hostView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + [self.container addSubview:hostView]; + + UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; + [self.container addGestureRecognizer:panGesture]; + } + + if (!self.dataSource) { + self.dataSource = [[EXPerfMonitorDataSource alloc] initWithBridge:_bridge host:self.currentHost]; + self.dataSource.delegate = self; + } else { + self.dataSource.host = self.currentHost; + } + + [self attachContainerIfNeeded]; + [self.dataSource startMonitoring]; + + RCTDevSettings *settings = (RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]; + if (settings && !settings.isPerfMonitorShown) { + settings.isPerfMonitorShown = YES; + } +} + +- (void)hide +{ + RCTDevSettings *settings = (RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]; + if (settings && settings.isPerfMonitorShown) { + settings.isPerfMonitorShown = NO; + } + + [self.container removeFromSuperview]; + + [self.dataSource stopMonitoring]; + self.dataSource.host = nil; + self.dataSource.delegate = nil; + self.dataSource = nil; + + [self.presenter clearContentSizeHandler]; + self.presenter = nil; + self.container = nil; + self.currentHost = nil; +} + +- (void)attachContainerIfNeeded +{ + UIWindow *window = RCTSharedApplication().delegate.window ?: RCTKeyWindow(); + if (!window) { + return; + } + + if (!self.container.superview) { + CGSize initialSize = [self.presenter currentContentSizeValue].CGSizeValue; + if (CGSizeEqualToSize(initialSize, CGSizeZero)) { + initialSize = CGSizeMake(EXPerfMonitorDefaultWidth, EXPerfMonitorMinimumHeight); + } + self.container.frame = [self initialFrameForWindow:window targetSize:initialSize]; + self.presenter.view.frame = self.container.bounds; + [window addSubview:self.container]; + } + + [self bringContainerToFront]; +} + +- (CGRect)initialFrameForWindow:(UIWindow *)window targetSize:(CGSize)size +{ + CGFloat targetWidth = EXPerfMonitorTargetWidthForWindow(window); + CGFloat width = MAX(MIN(size.width, targetWidth), EXPerfMonitorDefaultWidth); + CGFloat height = MAX(size.height, EXPerfMonitorMinimumHeight); + CGFloat originX = (window.bounds.size.width - width) / 2.0; + CGFloat originY = window.safeAreaInsets.top + EXPerfMonitorTopMargin; + + return CGRectMake(originX, originY, width, height); +} + +- (void)updateContainerForContentSize:(CGSize)contentSize +{ + if (!self.container || !self.presenter) { + return; + } + + CGRect frame = self.container.frame; + if (CGRectEqualToRect(frame, CGRectZero)) { + return; + } + + UIWindow *window = self.container.window ?: RCTSharedApplication().delegate.window ?: RCTKeyWindow(); + CGFloat targetWidth = EXPerfMonitorTargetWidthForWindow(window); + + CGSize adjustedSize; + if (CGSizeEqualToSize(contentSize, CGSizeZero)) { + adjustedSize = CGSizeMake(targetWidth, EXPerfMonitorMinimumHeight); + } else { + adjustedSize.width = MAX(MIN(contentSize.width, targetWidth), EXPerfMonitorDefaultWidth); + adjustedSize.height = MAX(contentSize.height, EXPerfMonitorMinimumHeight); + } + + frame.size = adjustedSize; + self.container.frame = frame; + self.presenter.view.frame = self.container.bounds; + [self bringContainerToFront]; +} + +- (void)bringContainerToFront +{ + UIView *superview = self.container.superview; + if (superview && superview.subviews.lastObject != self.container) { + [superview bringSubviewToFront:self.container]; + } +} + +- (void)perfMonitorDidUpdateStats:(EXPerfMonitorStatsSnapshot *)stats +{ + if (!self.presenter) { + return; + } + [self.presenter updateStatsWithMemoryMB:@(stats.memoryMB) + heapMB:@(stats.heapMB) + layoutDurationMS:@(stats.layoutDurationMS)]; +} + +- (void)perfMonitorDidUpdateFPS:(EXPerfMonitorFPSState *)fpsState track:(EXPerfMonitorTrack)track +{ + if (!self.presenter) { + return; + } + NSArray *history = fpsState.history ?: @[]; + PerfMonitorTrack bridgeTrack = (track == EXPerfMonitorTrackUI) ? PerfMonitorTrackUi : PerfMonitorTrackJs; + [self.presenter updateTrack:bridgeTrack currentFPS:@(fpsState.currentFPS) history:history]; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return nullptr; +} + +@end diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXPerfMonitorDataSource.h b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXPerfMonitorDataSource.h new file mode 100644 index 00000000000000..ec0bfe74baa335 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXPerfMonitorDataSource.h @@ -0,0 +1,64 @@ +// Copyright 2025-present 650 Industries. All rights reserved. + +#import + +@class RCTBridge; +@class RCTHost; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, EXPerfMonitorTrack) { + EXPerfMonitorTrackUI, + EXPerfMonitorTrackJS +}; + +// Captures FPS samples and other metrics emitted by the legacy React Native perf monitor. +// The implementation is based on `RCTPerfMonitor` from RN, but split out so the UI can be +// replaced with our SwiftUI overlay. +@interface EXPerfMonitorFPSState : NSObject + +@property (nonatomic, readonly) NSUInteger currentFPS; +@property (nonatomic, strong, readonly) NSArray *history; + +- (instancetype)initWithCurrentFPS:(NSUInteger)currentFPS + history:(NSArray *)history NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +@interface EXPerfMonitorStatsSnapshot : NSObject + +@property (nonatomic, readonly) double memoryMB; +@property (nonatomic, readonly) double heapMB; +@property (nonatomic, readonly) double layoutDurationMS; + +- (instancetype)initWithMemoryMB:(double)memoryMB + heapMB:(double)heapMB + layoutDurationMS:(double)layoutDurationMS NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +@protocol EXPerfMonitorDataSourceDelegate + +- (void)perfMonitorDidUpdateStats:(EXPerfMonitorStatsSnapshot *)stats; +- (void)perfMonitorDidUpdateFPS:(EXPerfMonitorFPSState *)fpsState track:(EXPerfMonitorTrack)track; + +@end + +@interface EXPerfMonitorDataSource : NSObject + +@property (nonatomic, weak, nullable) id delegate; + +- (instancetype)initWithBridge:(RCTBridge *)bridge host:(nullable RCTHost *)host; + +@property (nonatomic, weak, nullable) RCTHost *host; + +- (void)startMonitoring; +- (void)stopMonitoring; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXPerfMonitorDataSource.mm b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXPerfMonitorDataSource.mm new file mode 100644 index 00000000000000..74132b82997599 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/EXPerfMonitorDataSource.mm @@ -0,0 +1,411 @@ +// Copyright 2025-present 650 Industries. All rights reserved. +// +// This file copies the behaviour of React Native's `RCTPerfMonitor` module but strips out +// its UIKit overlay. We keep the sampling logic (FPS links, memory etc.) so Expo Go can +// present the data inside a custom SwiftUI view. + +#import "EXPerfMonitorDataSource.h" + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#include +#include +#include +#include +#include + +#if RCT_DEV + +static const NSUInteger EXPerfMonitorHistoryLength = 40; +static const NSTimeInterval EXPerfMonitorStatsUpdateInterval = 1.0; + +static vm_size_t EXPerfMonitorResidentMemorySize(void) +{ + vm_size_t memoryUsageInByte = 0; + task_vm_info_data_t vmInfo; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count); + if (kernelReturn == KERN_SUCCESS) { + memoryUsageInByte = (vm_size_t)vmInfo.phys_footprint; + } + return memoryUsageInByte; +} + +@interface EXPerfMonitorFPSCounter : NSObject + +- (instancetype)initWithHistoryLength:(NSUInteger)historyLength; +- (nullable EXPerfMonitorFPSState *)recordTimestamp:(NSTimeInterval)timestamp; +- (void)reset; + +@end + +@implementation EXPerfMonitorFPSState + +- (instancetype)initWithCurrentFPS:(NSUInteger)currentFPS + history:(NSArray *)history +{ + if ((self = [super init])) { + _currentFPS = currentFPS; + _history = [history copy]; + } + return self; +} + +@end + +@implementation EXPerfMonitorStatsSnapshot + +- (instancetype)initWithMemoryMB:(double)memoryMB + heapMB:(double)heapMB + layoutDurationMS:(double)layoutDurationMS +{ + if ((self = [super init])) { + _memoryMB = memoryMB; + _heapMB = heapMB; + _layoutDurationMS = layoutDurationMS; + } + return self; +} + +@end + +@implementation EXPerfMonitorFPSCounter { + NSUInteger _historyLength; + NSMutableArray *_history; + NSTimeInterval _previousTimestamp; + NSUInteger _frameCount; + NSUInteger _currentFPS; +} + +- (instancetype)initWithHistoryLength:(NSUInteger)historyLength +{ + if ((self = [super init])) { + _historyLength = historyLength; + _history = [NSMutableArray arrayWithCapacity:historyLength]; + _previousTimestamp = -1; + _frameCount = 0; + _currentFPS = 0; + } + return self; +} + +- (void)reset +{ + @synchronized(self) { + _history = [NSMutableArray arrayWithCapacity:_historyLength]; + _previousTimestamp = -1; + _frameCount = 0; + _currentFPS = 0; + } +} + +- (nullable EXPerfMonitorFPSState *)recordTimestamp:(NSTimeInterval)timestamp +{ + @synchronized(self) { + _frameCount++; + if (_previousTimestamp < 0) { + _previousTimestamp = timestamp; + return nil; + } + + NSTimeInterval delta = timestamp - _previousTimestamp; + if (delta < 1) { + return nil; + } + + NSUInteger fps = (NSUInteger)round((double)_frameCount / delta); + _currentFPS = fps; + + if (_history.count >= _historyLength) { + [_history removeObjectAtIndex:0]; + } + [_history addObject:@(fps)]; + + _previousTimestamp = timestamp; + _frameCount = 0; + + return [[EXPerfMonitorFPSState alloc] initWithCurrentFPS:_currentFPS + history:_history]; + } +} + +@end + +@interface EXPerfMonitorDataSource () + +@property (nonatomic, weak) RCTBridge *bridge; +@property (nonatomic, assign) BOOL monitoring; +@property (nonatomic, strong) CADisplayLink *uiDisplayLink; +@property (nonatomic, strong) CADisplayLink *jsDisplayLink; +@property (nonatomic, strong) EXPerfMonitorFPSCounter *uiFPSCounter; +@property (nonatomic, strong) EXPerfMonitorFPSCounter *jsFPSCounter; +@property (nonatomic, assign) double hermesHeapSizeInMB; +@property (nonatomic, assign) BOOL observingSurfacePresenter; + +@end + +@implementation EXPerfMonitorDataSource { + facebook::react::RuntimeExecutor _runtimeExecutor; + double _lastLayoutDurationMS; +} + +@synthesize host = _host; + +- (instancetype)initWithBridge:(RCTBridge *)bridge host:(RCTHost *)host +{ + if ((self = [super init])) { + _bridge = bridge; + _uiFPSCounter = [[EXPerfMonitorFPSCounter alloc] initWithHistoryLength:EXPerfMonitorHistoryLength]; + _jsFPSCounter = [[EXPerfMonitorFPSCounter alloc] initWithHistoryLength:EXPerfMonitorHistoryLength]; + _hermesHeapSizeInMB = 0; + _lastLayoutDurationMS = 0; + self.host = host; + } + return self; +} + +- (void)setHost:(RCTHost *)host +{ + if (_host == host) { + return; + } + if (_host && self.observingSurfacePresenter) { + [_host.surfacePresenter removeObserver:self]; + self.observingSurfacePresenter = NO; + } + _host = host; + _hermesHeapSizeInMB = 0; + _runtimeExecutor = facebook::react::RuntimeExecutor(); + _lastLayoutDurationMS = 0; + if (_monitoring) { + [self attachSurfacePresenterObserverIfNeeded]; + } +} + +- (void)startMonitoring +{ + if (self.monitoring || !_bridge) { + return; + } + + self.monitoring = YES; + self.hermesHeapSizeInMB = 0; + [self.uiFPSCounter reset]; + [self.jsFPSCounter reset]; + _lastLayoutDurationMS = 0; + + [self attachSurfacePresenterObserverIfNeeded]; + [self startDisplayLinks]; + [self captureHermesHeapInfo]; + + __weak __typeof(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateStats]; + }); +} + +- (void)stopMonitoring +{ + if (!self.monitoring) { + return; + } + + self.monitoring = NO; + + [self stopDisplayLinks]; + [self detachSurfacePresenterObserverIfNeeded]; + self.hermesHeapSizeInMB = 0; + _runtimeExecutor = facebook::react::RuntimeExecutor(); + _lastLayoutDurationMS = 0; +} + +- (void)dealloc +{ + [self detachSurfacePresenterObserverIfNeeded]; + [self stopMonitoring]; +} + +#pragma mark - Display links + +- (void)startDisplayLinks +{ + if (!self.uiDisplayLink) { + self.uiDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleUIDisplayLink:)]; + [self.uiDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + + __weak __typeof(self) weakSelf = self; + [_bridge dispatchBlock:^{ + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf || strongSelf.jsDisplayLink) { + return; + } + strongSelf.jsDisplayLink = [CADisplayLink displayLinkWithTarget:strongSelf selector:@selector(handleJSDisplayLink:)]; + [strongSelf.jsDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + } queue:RCTJSThread]; +} + +- (void)stopDisplayLinks +{ + [self.uiDisplayLink invalidate]; + self.uiDisplayLink = nil; + + [self.jsDisplayLink invalidate]; + self.jsDisplayLink = nil; +} + +- (void)handleUIDisplayLink:(CADisplayLink *)displayLink +{ + [self handleDisplayLink:displayLink forTrack:EXPerfMonitorTrackUI]; +} + +- (void)handleJSDisplayLink:(CADisplayLink *)displayLink +{ + [self handleDisplayLink:displayLink forTrack:EXPerfMonitorTrackJS]; +} + +- (void)handleDisplayLink:(CADisplayLink *)displayLink forTrack:(EXPerfMonitorTrack)track +{ + EXPerfMonitorFPSCounter *counter = (track == EXPerfMonitorTrackUI) ? self.uiFPSCounter : self.jsFPSCounter; + EXPerfMonitorFPSState *state = [counter recordTimestamp:displayLink.timestamp]; + if (!state || !self.delegate) { + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.monitoring) { + [self.delegate perfMonitorDidUpdateFPS:state track:track]; + } + }); +} + +- (void)updateStats +{ + if (!self.monitoring || !self.bridge) { + return; + } + + double memoryMB = (double)EXPerfMonitorResidentMemorySize() / 1024.0 / 1024.0; + [self captureHermesHeapInfo]; + double heapMB = self.hermesHeapSizeInMB; + double layoutDurationMS = _lastLayoutDurationMS; + + EXPerfMonitorStatsSnapshot *snapshot = + [[EXPerfMonitorStatsSnapshot alloc] initWithMemoryMB:memoryMB + heapMB:heapMB + layoutDurationMS:layoutDurationMS]; + + if (self.delegate) { + [self.delegate perfMonitorDidUpdateStats:snapshot]; + } + + __weak __typeof(self) weakSelf = self; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(EXPerfMonitorStatsUpdateInterval * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf && strongSelf.monitoring) { + [strongSelf updateStats]; + } + }); +} + +- (void)attachSurfacePresenterObserverIfNeeded +{ + if (self.observingSurfacePresenter || !self.host) { + return; + } + [self.host.surfacePresenter addObserver:self]; + self.observingSurfacePresenter = YES; +} + +- (void)detachSurfacePresenterObserverIfNeeded +{ + if (self.observingSurfacePresenter && self.host) { + [self.host.surfacePresenter removeObserver:self]; + } + self.observingSurfacePresenter = NO; +} + +- (void)updateLayoutDurationForRootTag:(NSInteger)rootTag +{ + _lastLayoutDurationMS = 0; + + RCTHost *host = self.host; + if (!host || !host.surfacePresenter) { + return; + } + + RCTFabricSurface *surface = [host.surfacePresenter surfaceForRootTag:rootTag]; + if (!surface) { + return; + } + + const facebook::react::SurfaceHandler &surfaceHandler = [surface surfaceHandler]; + auto mountingCoordinator = surfaceHandler.getMountingCoordinator(); + if (!mountingCoordinator) { + return; + } + + auto revision = mountingCoordinator->getBaseRevision(); + const auto layoutStart = revision.telemetry.getLayoutStartTime(); + const auto layoutEnd = revision.telemetry.getLayoutEndTime(); + + if (layoutStart != facebook::react::kTelemetryUndefinedTimePoint && + layoutEnd != facebook::react::kTelemetryUndefinedTimePoint && + layoutEnd >= layoutStart) { + auto duration = layoutEnd - layoutStart; + _lastLayoutDurationMS = std::chrono::duration(duration).count(); + } +} + +- (void)didMountComponentsWithRootTag:(NSInteger)rootTag +{ + if (!self.monitoring) { + return; + } + [self updateLayoutDurationForRootTag:rootTag]; +} + +- (void)captureHermesHeapInfo +{ + if (!_runtimeExecutor) { + RCTHost *host = self.host; + if (!host) { + self.hermesHeapSizeInMB = 0; + return; + } + _runtimeExecutor = host.surfacePresenter.runtimeExecutor; + } + + if (!_runtimeExecutor) { + self.hermesHeapSizeInMB = 0; + return; + } + + __weak __typeof(self) weakSelf = self; + _runtimeExecutor([weakSelf](facebook::jsi::Runtime &runtime) { + auto &instrumentation = runtime.instrumentation(); + auto heapInfo = instrumentation.getHeapInfo(true); + auto iterator = heapInfo.find("hermes_allocatedBytes"); + double usedBytes = (iterator != heapInfo.end()) ? static_cast(iterator->second) : 0; + + dispatch_async(dispatch_get_main_queue(), ^{ + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf || !strongSelf.monitoring) { + return; + } + strongSelf.hermesHeapSizeInMB = usedBytes > 0 ? (usedBytes / 1024.0 / 1024.0) : 0; + }); + }); +} + +@end + +#endif diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorHostingController.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorHostingController.swift new file mode 100644 index 00000000000000..8081180ba89ab1 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorHostingController.swift @@ -0,0 +1,36 @@ +// Copyright 2025-present 650 Industries. All rights reserved. + +import SwiftUI + +@objcMembers +final class PerfMonitorHostingController: UIHostingController { + var contentSizeDidChange: ((NSValue) -> Void)? + + init(viewModel: PerfMonitorViewModel) { + super.init(rootView: PerfMonitorView(viewModel: viewModel)) + configure() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("Not implemented") + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + notifySizeChange() + } + + private func configure() { + view.backgroundColor = .clear + notifySizeChange() + } + + private func notifySizeChange() { + let size = preferredContentSize == .zero ? view.intrinsicContentSize : preferredContentSize + guard size != .zero else { + return + } + contentSizeDidChange?(NSValue(cgSize: size)) + } +} diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorModels.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorModels.swift new file mode 100644 index 00000000000000..1a742a9f0c915a --- /dev/null +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorModels.swift @@ -0,0 +1,159 @@ +// Copyright 2025-present 650 Industries. All rights reserved. + +import Foundation +import SwiftUI +import UIKit + +enum PerfMonitorConstants { + static let maxWidth: CGFloat = 360 + static let screenWidthRatio: CGFloat = 0.95 +} + +@objc enum PerfMonitorTrack: Int { + case ui = 0 + case js = 1 + + var displayName: String { + switch self { + case .ui: + return "UI" + case .js: + return "JS" + } + } +} + +struct PerfMonitorSnapshot: Equatable { + var uiTrack: PerfMonitorTrackSnapshot + var jsTrack: PerfMonitorTrackSnapshot + var memoryMB: Double + var heapMB: Double + var layoutDurationMS: Double + + var formattedMemory: String { String(format: "%.2f", memoryMB) } + var formattedHeap: String { String(format: "%.2f", heapMB) } + var formattedLayoutDuration: String { String(format: "%.1f", layoutDurationMS) } +} + +struct PerfMonitorTrackSnapshot: Equatable { + let label: String + var currentFPS: Int + var history: [Double] + var formattedFPS: String { "\(currentFPS) fps" } +} + +@objcMembers +@MainActor +final class PerfMonitorViewModel: NSObject, ObservableObject { + @Published private(set) var snapshot: PerfMonitorSnapshot + private var onClose: (() -> Void)? + + override init() { + snapshot = PerfMonitorSnapshot( + uiTrack: PerfMonitorTrackSnapshot( + label: PerfMonitorTrack.ui.displayName, + currentFPS: 0, + history: [] + ), + jsTrack: PerfMonitorTrackSnapshot( + label: PerfMonitorTrack.js.displayName, + currentFPS: 0, + history: [] + ), + memoryMB: 0, + heapMB: 0, + layoutDurationMS: 0 + ) + super.init() + } + + func updateStats(memoryMB: NSNumber, heapMB: NSNumber, layoutDurationMS: NSNumber) { + snapshot.memoryMB = memoryMB.doubleValue + snapshot.heapMB = heapMB.doubleValue + snapshot.layoutDurationMS = layoutDurationMS.doubleValue + } + + func updateTrack(_ track: PerfMonitorTrack, currentFPS: NSNumber, history: [NSNumber]) { + let trackSnapshot = PerfMonitorTrackSnapshot( + label: track.displayName, + currentFPS: currentFPS.intValue, + history: history.map(\.doubleValue) + ) + + switch track { + case .ui: + snapshot.uiTrack = trackSnapshot + case .js: + snapshot.jsTrack = trackSnapshot + } + } + + func setCloseHandler(_ handler: @escaping () -> Void) { + onClose = handler + } + + func closeMonitor() { + onClose?() + } + + func clearCloseHandler() { + onClose = nil + } +} + +@objc(EXPerfMonitorPresenter) +@objcMembers +@MainActor +final class PerfMonitorPresenter: NSObject { + private let viewModel: PerfMonitorViewModel + private let hostingController: PerfMonitorHostingController + + override init() { + viewModel = PerfMonitorViewModel() + hostingController = PerfMonitorHostingController(viewModel: viewModel) + super.init() + } + + var view: UIView { + hostingController.view + } + + func setContentSizeHandler(_ handler: @escaping (NSValue) -> Void) { + hostingController.contentSizeDidChange = handler + } + + func clearContentSizeHandler() { + hostingController.contentSizeDidChange = nil + viewModel.clearCloseHandler() + } + + func currentContentSizeValue() -> NSValue { + let preferredSize = hostingController.preferredContentSize + guard preferredSize == .zero else { + return NSValue(cgSize: preferredSize) + } + + let intrinsic = hostingController.view.intrinsicContentSize + guard intrinsic == .zero else { + return NSValue(cgSize: intrinsic) + } + + let targetWidth = min( + UIScreen.main.bounds.width * PerfMonitorConstants.screenWidthRatio, + PerfMonitorConstants.maxWidth + ) + return NSValue(cgSize: CGSize(width: targetWidth, height: 176)) + } + + func updateStats(memoryMB: NSNumber, heapMB: NSNumber, layoutDurationMS: NSNumber) { + viewModel.updateStats(memoryMB: memoryMB, heapMB: heapMB, layoutDurationMS: layoutDurationMS) + } + + func updateTrack(_ track: PerfMonitorTrack, currentFPS: NSNumber, history: [NSNumber]) { + viewModel.updateTrack(track, currentFPS: currentFPS, history: history) + } + + func setCloseHandler(_ handler: @escaping () -> Void) { + viewModel.setCloseHandler(handler) + } +} diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorView.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorView.swift new file mode 100644 index 00000000000000..8dab509e2ec4d2 --- /dev/null +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Internal/DevSupport/PerfMonitor/PerfMonitorView.swift @@ -0,0 +1,220 @@ +// Copyright 2025-present 650 Industries. All rights reserved. + +import SwiftUI +import UIKit + +struct PerfMonitorView: View { + @ObservedObject var viewModel: PerfMonitorViewModel + + private static let cardCornerRadius: CGFloat = 18 + private static let graphHeight: CGFloat = 58 + private static let cardBackground = Color(red: 0.11, green: 0.12, blue: 0.16) + private static let accentColor = Color(red: 0.27, green: 0.55, blue: 0.98) + private static let borderColor = Color.white.opacity(0.08) + + private var cardWidth: CGFloat { + min(UIScreen.main.bounds.width * PerfMonitorConstants.screenWidthRatio, PerfMonitorConstants.maxWidth) + } + + var body: some View { + VStack(spacing: 12) { + header + + VStack(spacing: 12) { + fpsSection + statsSection + } + } + .padding(16) + .frame(width: cardWidth) + .background(Self.cardBackground) + .clipShape(RoundedRectangle(cornerRadius: Self.cardCornerRadius, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: Self.cardCornerRadius, style: .continuous) + .stroke(Self.borderColor, lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.5), radius: 22, x: 0, y: 12) + .preferredColorScheme(.dark) + } + + private var header: some View { + HStack(spacing: 12) { + Image(systemName: "arrow.up.and.down.and.arrow.left.and.right") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(Color.white.opacity(0.75)) + .frame(width: 28, height: 28) + Spacer() + Text("Performance monitor") + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .foregroundColor(.white.opacity(0.95)) + Spacer() + Button(action: { + viewModel.closeMonitor() + }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(Color.white.opacity(0.8)) + .font(.system(size: 20, weight: .semibold)) + } + .buttonStyle(.plain) + } + } + + private var fpsSection: some View { + HStack(spacing: 12) { + PerfMonitorTrackView( + snapshot: viewModel.snapshot.uiTrack, + accentColor: Self.accentColor, + height: Self.graphHeight + ) + + PerfMonitorTrackView( + snapshot: viewModel.snapshot.jsTrack, + accentColor: Self.accentColor, + height: Self.graphHeight + ) + } + } + + private var statsSection: some View { + HStack(spacing: 12) { + PerfMonitorStatCard( + title: "RAM", + value: viewModel.snapshot.formattedMemory, + unit: "MB", + ) + PerfMonitorStatCard( + title: "Hermes", + value: viewModel.snapshot.formattedHeap, + unit: "MB", + ) + PerfMonitorStatCard( + title: "Layout", + value: viewModel.snapshot.formattedLayoutDuration, + unit: "ms" + ) + } + } +} + +private struct PerfMonitorTrackView: View { + let snapshot: PerfMonitorTrackSnapshot + let accentColor: Color + let height: CGFloat + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + GraphView(values: snapshot.history, accentColor: accentColor) + .frame(height: height) + + HStack { + Text(snapshot.label.uppercased()) + .font(.caption) + .foregroundColor(Color.white.opacity(0.65)) + Spacer() + Text(snapshot.formattedFPS) + .font(.system(size: 15, weight: .semibold, design: .rounded)) + .foregroundColor(.white) + } + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color.white.opacity(0.08)) + ) + } +} + +private struct GraphView: View { + static let minFPS: Double = 0 + static let maxFPS: Double = 120 + + let values: [Double] + let accentColor: Color + + var body: some View { + GeometryReader { proxy in + let points = normalizedPoints(in: proxy.size) + ZStack(alignment: .bottomLeading) { + gradientFill(for: points, in: proxy.size) + linePath(for: points) + } + } + } + + private func gradientFill(for points: [CGPoint], in size: CGSize) -> some View { + LinearGradient( + gradient: Gradient(colors: [accentColor.opacity(0.5), accentColor.opacity(0.08)]), + startPoint: .top, + endPoint: .bottom + ) + .mask( + Path { path in + guard let first = points.first else { + return + } + path.move(to: CGPoint(x: first.x, y: size.height)) + path.addLine(to: first) + points.forEach { path.addLine(to: $0) } + path.addLine(to: CGPoint(x: points.last?.x ?? size.width, y: size.height)) + path.closeSubpath() + } + ) + } + + private func linePath(for points: [CGPoint]) -> some View { + Path { path in + guard let first = points.first else { + return + } + path.move(to: first) + points.dropFirst().forEach { path.addLine(to: $0) } + } + .stroke(accentColor, style: StrokeStyle(lineWidth: 2.2, lineCap: .round, lineJoin: .round)) + } + + private func normalizedPoints(in size: CGSize) -> [CGPoint] { + guard !values.isEmpty else { + return [] + } + + let clampedValues = values.map { min(max($0, Self.minFPS), Self.maxFPS) } + let range = Self.maxFPS - Self.minFPS + let stepX = size.width / CGFloat(max(values.count - 1, 1)) + + return clampedValues.enumerated().map { index, value in + let normalized = (value - Self.minFPS) / range + return CGPoint( + x: CGFloat(index) * stepX, + y: size.height * (1 - CGFloat(normalized)) + ) + } + } +} + +private struct PerfMonitorStatCard: View { + let title: String + let value: String + let unit: String + + var body: some View { + VStack(spacing: 6) { + Text(title) + .font(.caption) + .foregroundColor(.white.opacity(0.6)) + HStack(alignment: .firstTextBaseline, spacing: 2) { + Text(value) + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundColor(.white) + Text(unit) + .font(.caption2) + .foregroundColor(.white.opacity(0.6)) + } + } + .frame(maxWidth: .infinity) + .frame(height: 60) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(.white.opacity(0.08)) + ) + } +} diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift index c4b5741cfaba9e..711ce86ab5a429 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/EXScopedNotificationSerializer.swift @@ -1,7 +1,7 @@ // Copyright 2018-present 650 Industries. All rights reserved. import UserNotifications -import EXNotifications +import ExpoNotifications public class EXScopedNotificationSerializer { diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsCategoriesModule.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsCategoriesModule.swift index d98f2630e4c052..65c9b7fb5e319e 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsCategoriesModule.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsCategoriesModule.swift @@ -1,7 +1,7 @@ // Copyright 2025-present 650 Industries. All rights reserved. import ExpoModulesCore -import EXNotifications +import ExpoNotifications public final class ExpoGoNotificationsCategoriesModule: CategoriesModule { private let scopeKey: String diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsEmitterModule.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsEmitterModule.swift index 8579cc49e18fa7..d7de186fadc821 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsEmitterModule.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsEmitterModule.swift @@ -1,7 +1,7 @@ // Copyright 2025-present 650 Industries. All rights reserved. import ExpoModulesCore -import EXNotifications +import ExpoNotifications public final class ExpoGoNotificationsEmitterModule: EmitterModule { private let scopeKey: String diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsHandlerModule.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsHandlerModule.swift index 19e2a4eeff5466..ce73c3ddad6886 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsHandlerModule.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsHandlerModule.swift @@ -1,7 +1,7 @@ // Copyright 2025-present 650 Industries. All rights reserved. import ExpoModulesCore -import EXNotifications +import ExpoNotifications public final class ExpoGoNotificationsHandlerModule: HandlerModule { private let scopeKey: String diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsPresentationModule.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsPresentationModule.swift index fdc6b3ed3e9e80..b79e5cf31e2cbe 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsPresentationModule.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsPresentationModule.swift @@ -1,7 +1,7 @@ // Copyright 2025-present 650 Industries. All rights reserved. import ExpoModulesCore -import EXNotifications +import ExpoNotifications public final class ExpoGoNotificationsPresentationModule: PresentationModule { private let scopeKey: String diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsSchedulerModule.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsSchedulerModule.swift index c4cea5e3d95a68..421cb51617601e 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsSchedulerModule.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsSchedulerModule.swift @@ -1,7 +1,7 @@ // Copyright 2025-present 650 Industries. All rights reserved. import ExpoModulesCore -import EXNotifications +import ExpoNotifications public final class ExpoGoNotificationsSchedulerModule: SchedulerModule { private let scopeKey: String diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsServerRegistrationModule.swift b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsServerRegistrationModule.swift index e20d379b1fe52d..5238ec64448466 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsServerRegistrationModule.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/Modules/Notifications/ExpoGoNotificationsServerRegistrationModule.swift @@ -1,7 +1,7 @@ // Copyright 2025-present 650 Industries. All rights reserved. import ExpoModulesCore -import EXNotifications +import ExpoNotifications // swiftlint:disable:next type_name public final class ExpoGoNotificationsServerRegistrationModule: ServerRegistrationModule { diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/UniversalModules/EXScopedSecureStore.m b/apps/expo-go/ios/Exponent/Versioned/Core/UniversalModules/EXScopedSecureStore.m index 6ff8d098ab6d59..738c9f0b76fd31 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/UniversalModules/EXScopedSecureStore.m +++ b/apps/expo-go/ios/Exponent/Versioned/Core/UniversalModules/EXScopedSecureStore.m @@ -68,7 +68,7 @@ - (void)migrateValue:(NSString *)value [self _deleteValueWithKey:scopedKey withOptions:options]; } else { - EXLogWarn(@"Encountered an error while saving SecureStore data: %@.", [[super class] _messageForError:error]); + RCTLogWarn(@"Encountered an error while saving SecureStore data: %@.", [[super class] _messageForError:error]); } } diff --git a/apps/expo-go/ios/Exponent/Versioned/Core/VersionManager.swift b/apps/expo-go/ios/Exponent/Versioned/Core/VersionManager.swift index d02240e584fc39..fe4aaf23b1a075 100644 --- a/apps/expo-go/ios/Exponent/Versioned/Core/VersionManager.swift +++ b/apps/expo-go/ios/Exponent/Versioned/Core/VersionManager.swift @@ -13,6 +13,7 @@ final class VersionManager: EXVersionManagerObjC { var appContext: AppContext? var legacyModulesProxy: LegacyNativeModulesProxy? var legacyModuleRegistry: EXModuleRegistry? + private var hasRegisteredExpoModules = false let params: [AnyHashable: Any] @@ -38,6 +39,39 @@ final class VersionManager: EXVersionManagerObjC { super.init(params: params, manifest: manifest, fatalHandler: fatalHandler, logFunction: logFunction, logThreshold: logThreshold) } + @objc + override func createExpoGoAppContext() -> AppContext { + return getOrCreateAppContext() + } + + private func getOrCreateAppContext() -> AppContext { + if let appContext { + if !hasRegisteredExpoModules { + registerExpoModules(appContext) + hasRegisteredExpoModules = true + } + return appContext + } + + let legacyModuleRegistry = createLegacyModuleRegistry(params: params, manifest: manifest) + let legacyModulesProxy = LegacyNativeModulesProxy(customModuleRegistry: legacyModuleRegistry) + let config = createAppContextConfig() + let appContext = AppContext( + legacyModulesProxy: legacyModulesProxy, + legacyModuleRegistry: legacyModuleRegistry, + config: config + ) + + self.appContext = appContext + self.legacyModuleRegistry = legacyModuleRegistry + self.legacyModulesProxy = legacyModulesProxy + + registerExpoModules(appContext) + hasRegisteredExpoModules = true + + return appContext + } + /** Invalidates the app context when the bridge is about to be rebuilt. */ @@ -53,14 +87,12 @@ final class VersionManager: EXVersionManagerObjC { override func extraModules() -> [Any] { // Ideally if we don't initialize the app context here, but unfortunately there is no better place in bridge lifecycle // that would work well for us (especially properly invalidating existing app context on reload). - let legacyModuleRegistry = createLegacyModuleRegistry(params: params, manifest: manifest) - let legacyModulesProxy = LegacyNativeModulesProxy(customModuleRegistry: legacyModuleRegistry) - let config = createAppContextConfig() - let appContext = AppContext(legacyModulesProxy: legacyModulesProxy, legacyModuleRegistry: legacyModuleRegistry, config: config) + let appContext = getOrCreateAppContext() - self.appContext = appContext - self.legacyModuleRegistry = legacyModuleRegistry - self.legacyModulesProxy = legacyModulesProxy + guard let legacyModulesProxy, + let legacyModuleRegistry else { + fatalError("Legacy modules should have been initialized") + } let modules: [Any] = [ EXAppState(), @@ -78,18 +110,14 @@ final class VersionManager: EXVersionManagerObjC { ExpoBridgeModule(appContext: appContext) ] - // Register additional Expo modules, specific to Expo Go. - registerExpoModules() - return modules + super.extraModules() } /** Registers Expo modules that are not generated in ``ExpoModulesProvider``, but are necessary for Expo Go apps. */ - private func registerExpoModules() { - guard let appContext, - let kernelServices = params["services"] as? [AnyHashable: Any] else { + private func registerExpoModules(_ appContext: AppContext) { + guard let kernelServices = params["services"] as? [AnyHashable: Any] else { log.error("Unable to register Expo modules, the app context or kernel services is unavailable") return } diff --git a/apps/expo-go/ios/ExponentIntegrationTests/ExponentIntegrationTests.m b/apps/expo-go/ios/ExponentIntegrationTests/ExponentIntegrationTests.m deleted file mode 100644 index f15b9f9b53090c..00000000000000 --- a/apps/expo-go/ios/ExponentIntegrationTests/ExponentIntegrationTests.m +++ /dev/null @@ -1,88 +0,0 @@ -/** - * This test launches the JS Expo app called `test-suite` and checks whether all the JS tests there pass. - * `test-suite` runs a bunch of jasmine JS tests against the Expo SDK. - * The purpose of this file is to provide a native pass/fail hook into the JS sdk tests. - * - * To configure it, make sure this target contains a file called `EXTestEnvironment.plist` - * with a key `testSuiteUrl` whose value is the url to load some version of Expo's `test-suite` app. - */ - -#import "ExpoKit.h" -#import "EXEnvironment.h" -#import "EXKernel.h" -#import "EXKernelLinkingManager.h" -#import "EXRootViewController.h" -#import "EXHomeAppManager.h" -#import "EXTest.h" - -#import -#import - -#import - -@interface ExponentIntegrationTests : XCTestCase - -@property (nonatomic, strong) EXRootViewController *rootViewController; -@property (nonatomic, strong) NSString *testSuiteUrl; - -@end - -@implementation ExponentIntegrationTests - -- (void)setUp -{ - [super setUp]; - [self _loadConfig]; - - _rootViewController = (EXRootViewController *)[ExpoKit sharedInstance].rootViewController; - // if test environment isn't configured for a shell app, override here - // since clearly we're running tests - if ([EXEnvironment sharedEnvironment].testEnvironment == EXTestEnvironmentNone) { - [EXEnvironment sharedEnvironment].testEnvironment = EXTestEnvironmentLocal; - } - - // NOTE(2018-02-20): Without giving the kernel a second to run, it never opens test-suite. With a - // cursory pass through the code, I didn't see the correct event to wait for. Perhaps after we - // implement a pure-native kernel, we'll be able to remove this shoddy delay. - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [[EXKernel sharedInstance].serviceRegistry.linkingManager openUrl:self->_testSuiteUrl isUniversalLink:NO]; - }); - -} - -- (void)testDoesTestSuiteAppPassAllJSTests -{ - XCTAssert((_testSuiteUrl), @"No url configured for JS test-suite. Make sure EXTestEnvironment.plist exists and contains a url to test-suite."); - - XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription: @"Run all JS integration tests"]; - - __block NSDictionary *jsTestSuiteResult = nil; - id observer = [NSNotificationCenter.defaultCenter - addObserverForName:EXTestSuiteCompletedNotification - object:nil - queue:NSOperationQueue.currentQueue - usingBlock:^(NSNotification *notification) { - jsTestSuiteResult = notification.userInfo; - [expectation fulfill]; - }]; - - [self waitForExpectations:@[expectation] timeout:180]; - [NSNotificationCenter.defaultCenter removeObserver:observer]; - - XCTAssert((jsTestSuiteResult), @"Test suite timed out"); - XCTAssert(([jsTestSuiteResult[@"failed"] integerValue] == 0), @"Test suite failed: %@", jsTestSuiteResult); -} - -#pragma mark - internal - -- (void)_loadConfig -{ - // This plist is generated with `powertools configure-ios-test-suite-url` - NSString *configPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"EXTestEnvironment" ofType:@"plist"]; - NSDictionary *testConfig = (configPath) ? [NSDictionary dictionaryWithContentsOfFile:configPath] : [NSDictionary dictionary]; - if (testConfig) { - _testSuiteUrl = testConfig[@"testSuiteUrl"]; - } -} - -@end diff --git a/apps/expo-go/ios/ExponentIntegrationTests/Info.plist b/apps/expo-go/ios/ExponentIntegrationTests/Info.plist deleted file mode 100644 index ba72822e8728ef..00000000000000 --- a/apps/expo-go/ios/ExponentIntegrationTests/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/apps/expo-go/ios/Podfile b/apps/expo-go/ios/Podfile index 7b234f906657fc..d12e5bcb865446 100644 --- a/apps/expo-go/ios/Podfile +++ b/apps/expo-go/ios/Podfile @@ -24,7 +24,7 @@ target 'Expo Go' do # Required by react-native-maps # See https://github.com/react-native-maps/react-native-maps/blob/master/docs/installation.md - pod 'react-native-google-maps', :path => '../../../node_modules/react-native-maps' + pod 'react-native-maps/Maps', :path => '../../../node_modules/react-native-maps' # Required by firebase core versions 9.x / 10.x (included with SDK 47) # See https://github.com/invertase/react-native-firebase/issues/6332#issuecomment-1189734581 @@ -50,9 +50,9 @@ target 'Expo Go' do 'expo-network-addons', 'expo-insights', 'expo-splash-screen', - 'expo-blob', '@expo/ui', - '@expo/app-integrity' + '@expo/app-integrity', + 'expo-brownfield' ], includeTests: true, flags: { @@ -154,17 +154,7 @@ target 'Expo Go' do end end - # Test targets - target 'ExponentIntegrationTests' do - inherit! :search_paths - end - target 'Tests' do - # `ExpoModulesTestCore` has implicit dependency to `React-Core` which has a resource bundle. - # To prevent CocoaPods generating new `React-Core` resource bundle and the strange `React-Core-60309c9c` target, - # this test target should inherit all properties from parents. - inherit! :complete - pod 'ExpoModulesTestCore', :path => "../../../packages/expo-modules-test-core/ios" end end diff --git a/apps/expo-go/ios/Podfile.lock b/apps/expo-go/ios/Podfile.lock index c19b968e1235b0..60980fbdd39fc8 100644 --- a/apps/expo-go/ios/Podfile.lock +++ b/apps/expo-go/ios/Podfile.lock @@ -11,9 +11,6 @@ PODS: - ExpoModulesTestCore - EXApplication (7.0.7): - ExpoModulesCore - - EXAV (16.0.7): - - ExpoModulesCore - - ReactCommon/turbomodule/core - EXConstants (18.0.9): - ExpoModulesCore - EXJSONUtils (0.15.0) @@ -23,11 +20,6 @@ PODS: - EXManifests/Tests (1.0.8): - ExpoModulesCore - ExpoModulesTestCore - - EXNotifications (0.32.11): - - ExpoModulesCore - - EXNotifications/Tests (0.32.11): - - ExpoModulesCore - - ExpoModulesTestCore - Expo (54.0.8): - boost - DoubleConversion @@ -166,6 +158,8 @@ PODS: - ExpoModulesCore - ExpoBattery (10.0.7): - ExpoModulesCore + - ExpoBlob (0.1.6): + - ExpoModulesCore - ExpoBlur (15.0.7): - ExpoModulesCore - ExpoBrightness (14.0.7): @@ -327,6 +321,11 @@ PODS: - React-hermes - ExpoNetwork (8.0.7): - ExpoModulesCore + - ExpoNotifications (0.32.11): + - ExpoModulesCore + - ExpoNotifications/Tests (0.32.11): + - ExpoModulesCore + - ExpoModulesTestCore - ExpoPrint (15.0.7): - ExpoModulesCore - ExpoRouter (6.0.6): @@ -381,6 +380,9 @@ PODS: - ExpoModulesCore - ExpoSystemUI (6.0.7): - ExpoModulesCore + - ExpoTaskManager (14.0.7): + - ExpoModulesCore + - UMAppLoader - ExpoTrackingTransparency (6.0.7): - ExpoModulesCore - ExpoVideo (3.0.11): @@ -389,11 +391,10 @@ PODS: - ExpoModulesCore - ExpoWebBrowser (15.0.7): - ExpoModulesCore + - ExpoWidgets (0.0.0): + - ExpoModulesCore - EXStructuredHeaders (5.0.0) - EXStructuredHeaders/Tests (5.0.0) - - EXTaskManager (14.0.7): - - ExpoModulesCore - - UMAppLoader - EXUpdates (29.0.10): - boost - DoubleConversion @@ -519,8 +520,6 @@ PODS: - PromisesSwift (~> 2.1) - fmt (11.0.2) - glog (0.3.5) - - Google-Maps-iOS-Utils (5.0.0): - - GoogleMaps (~> 8.0) - GoogleAppMeasurement (11.11.0): - GoogleAppMeasurement/AdIdSupport (= 11.11.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) @@ -544,11 +543,6 @@ PODS: - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - GoogleMaps (8.4.0): - - GoogleMaps/Maps (= 8.4.0) - - GoogleMaps/Base (8.4.0) - - GoogleMaps/Maps (8.4.0): - - GoogleMaps/Base - GoogleUtilities (8.0.2): - GoogleUtilities/AppDelegateSwizzler (= 8.0.2) - GoogleUtilities/Environment (= 8.0.2) @@ -2543,11 +2537,36 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - react-native-google-maps (1.20.1): - - Google-Maps-iOS-Utils (= 5.0.0) - - GoogleMaps (= 8.4.0) + - react-native-keyboard-controller (1.20.4): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety - React-Core - - react-native-keyboard-controller (1.18.5): + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-keyboard-controller/common (= 1.20.4) + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - react-native-keyboard-controller/common (1.20.4): - boost - DoubleConversion - fast_float @@ -2565,7 +2584,6 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-keyboard-controller/common (= 1.18.5) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2576,7 +2594,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-keyboard-controller/common (1.18.5): + - react-native-maps/Generated (1.26.20): - boost - DoubleConversion - fast_float @@ -2604,11 +2622,38 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-maps (1.20.1): + - react-native-maps/Maps (1.26.20): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-maps/Generated + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - react-native-netinfo (11.4.1): - React-Core - - react-native-pager-view (6.9.1): + - react-native-pager-view (8.0.0): - boost - DoubleConversion - fast_float @@ -2635,8 +2680,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket + - SwiftUIIntrospect (~> 1.0) - Yoga - - react-native-safe-area-context (5.6.0): + - react-native-safe-area-context (5.6.2): - boost - DoubleConversion - fast_float @@ -2654,8 +2700,8 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-safe-area-context/common (= 5.6.0) - - react-native-safe-area-context/fabric (= 5.6.0) + - react-native-safe-area-context/common (= 5.6.2) + - react-native-safe-area-context/fabric (= 5.6.2) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2666,7 +2712,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-safe-area-context/common (5.6.0): + - react-native-safe-area-context/common (5.6.2): - boost - DoubleConversion - fast_float @@ -2694,7 +2740,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-safe-area-context/fabric (5.6.0): + - react-native-safe-area-context/fabric (5.6.2): - boost - DoubleConversion - fast_float @@ -2725,7 +2771,7 @@ PODS: - Yoga - react-native-segmented-control (2.5.7): - React-Core - - react-native-skia (2.2.12): + - react-native-skia (2.4.14): - boost - DoubleConversion - fast_float @@ -2755,7 +2801,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-slider (5.0.1): + - react-native-slider (5.1.1): - boost - DoubleConversion - fast_float @@ -2773,7 +2819,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-slider/common (= 5.0.1) + - react-native-slider/common (= 5.1.1) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -2784,7 +2830,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-slider/common (5.0.1): + - react-native-slider/common (5.1.1): - boost - DoubleConversion - fast_float @@ -2840,7 +2886,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-webview (13.15.0): + - react-native-webview (13.16.0): - boost - DoubleConversion - fast_float @@ -3481,7 +3527,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNCPicker (2.11.2): + - RNCPicker (2.11.4): - boost - DoubleConversion - fast_float @@ -3509,7 +3555,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNDateTimePicker (8.4.4): + - RNDateTimePicker (8.6.0): - boost - DoubleConversion - fast_float @@ -3537,7 +3583,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNGestureHandler (2.28.0): + - RNGestureHandler (2.30.0): - boost - DoubleConversion - fast_float @@ -3565,7 +3611,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNReanimated (4.2.0): + - RNReanimated (4.2.1): - boost - DoubleConversion - fast_float @@ -3592,11 +3638,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 4.2.0) + - RNReanimated/reanimated (= 4.2.1) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated (4.2.0): + - RNReanimated/reanimated (4.2.1): - boost - DoubleConversion - fast_float @@ -3623,11 +3669,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 4.2.0) + - RNReanimated/reanimated/apple (= 4.2.1) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated/apple (4.2.0): + - RNReanimated/reanimated/apple (4.2.1): - boost - DoubleConversion - fast_float @@ -3716,7 +3762,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNSVG (15.12.1): + - RNSVG (15.15.1): - boost - DoubleConversion - fast_float @@ -3742,10 +3788,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.12.1) + - RNSVG/common (= 15.15.1) - SocketRocket - Yoga - - RNSVG/common (15.12.1): + - RNSVG/common (15.15.1): - boost - DoubleConversion - fast_float @@ -3874,13 +3920,13 @@ PODS: - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - SocketRocket (0.7.1) - - Stripe (24.19.0): - - StripeApplePay (= 24.19.0) - - StripeCore (= 24.19.0) - - StripePayments (= 24.19.0) - - StripePaymentsUI (= 24.19.0) - - StripeUICore (= 24.19.0) - - stripe-react-native (0.50.3): + - Stripe (25.0.1): + - StripeApplePay (= 25.0.1) + - StripeCore (= 25.0.1) + - StripePayments (= 25.0.1) + - StripePaymentsUI (= 25.0.1) + - StripeUICore (= 25.0.1) + - stripe-react-native (0.57.2): - boost - DoubleConversion - fast_float @@ -3907,15 +3953,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - Stripe (~> 24.19.0) - - stripe-react-native/NewArch (= 0.50.3) - - StripeApplePay (~> 24.19.0) - - StripeFinancialConnections (~> 24.19.0) - - StripePayments (~> 24.19.0) - - StripePaymentSheet (~> 24.19.0) - - StripePaymentsUI (~> 24.19.0) + - stripe-react-native/Core (= 0.57.2) + - stripe-react-native/NewArch (= 0.57.2) - Yoga - - stripe-react-native/NewArch (0.50.3): + - stripe-react-native/Core (0.57.2): - boost - DoubleConversion - fast_float @@ -3942,35 +3983,64 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - Stripe (~> 24.19.0) - - StripeApplePay (~> 24.19.0) - - StripeFinancialConnections (~> 24.19.0) - - StripePayments (~> 24.19.0) - - StripePaymentSheet (~> 24.19.0) - - StripePaymentsUI (~> 24.19.0) + - Stripe (~> 25.0.1) + - StripeApplePay (~> 25.0.1) + - StripeFinancialConnections (~> 25.0.1) + - StripePayments (~> 25.0.1) + - StripePaymentSheet (~> 25.0.1) + - StripePaymentsUI (~> 25.0.1) - Yoga - - StripeApplePay (24.19.0): - - StripeCore (= 24.19.0) - - StripeCore (24.19.0) - - StripeFinancialConnections (24.19.0): - - StripeCore (= 24.19.0) - - StripeUICore (= 24.19.0) - - StripePayments (24.19.0): - - StripeCore (= 24.19.0) - - StripePayments/Stripe3DS2 (= 24.19.0) - - StripePayments/Stripe3DS2 (24.19.0): - - StripeCore (= 24.19.0) - - StripePaymentSheet (24.19.0): - - StripeApplePay (= 24.19.0) - - StripeCore (= 24.19.0) - - StripePayments (= 24.19.0) - - StripePaymentsUI (= 24.19.0) - - StripePaymentsUI (24.19.0): - - StripeCore (= 24.19.0) - - StripePayments (= 24.19.0) - - StripeUICore (= 24.19.0) - - StripeUICore (24.19.0): - - StripeCore (= 24.19.0) + - stripe-react-native/NewArch (0.57.2): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - StripeApplePay (25.0.1): + - StripeCore (= 25.0.1) + - StripeCore (25.0.1) + - StripeFinancialConnections (25.0.1): + - StripeCore (= 25.0.1) + - StripeUICore (= 25.0.1) + - StripePayments (25.0.1): + - StripeCore (= 25.0.1) + - StripePayments/Stripe3DS2 (= 25.0.1) + - StripePayments/Stripe3DS2 (25.0.1): + - StripeCore (= 25.0.1) + - StripePaymentSheet (25.0.1): + - StripeApplePay (= 25.0.1) + - StripeCore (= 25.0.1) + - StripePayments (= 25.0.1) + - StripePaymentsUI (= 25.0.1) + - StripePaymentsUI (25.0.1): + - StripeCore (= 25.0.1) + - StripePayments (= 25.0.1) + - StripeUICore (= 25.0.1) + - StripeUICore (25.0.1): + - StripeCore (= 25.0.1) + - SwiftUIIntrospect (1.3.0) - UMAppLoader (6.0.7) - Yoga (0.0.0) - ZXingObjC/Core (3.6.9) @@ -3986,14 +4056,11 @@ DEPENDENCIES: - EASClient (from `../../../packages/expo-eas-client/ios`) - EASClient/Tests (from `../../../packages/expo-eas-client/ios`) - EXApplication (from `../../../packages/expo-application/ios`) - - EXAV (from `../../../node_modules/expo-av/ios`) - EXConstants (from `../../../packages/expo-constants/ios`) - EXJSONUtils (from `../../../packages/expo-json-utils/ios`) - EXJSONUtils/Tests (from `../../../packages/expo-json-utils/ios`) - EXManifests (from `../../../packages/expo-manifests/ios`) - EXManifests/Tests (from `../../../packages/expo-manifests/ios`) - - EXNotifications (from `../../../packages/expo-notifications/ios`) - - EXNotifications/Tests (from `../../../packages/expo-notifications/ios`) - Expo (from `../../../packages/expo`) - expo-dev-menu (from `../../../packages/expo-dev-menu`) - expo-dev-menu-interface (from `../../../packages/expo-dev-menu-interface/ios`) @@ -4005,6 +4072,7 @@ DEPENDENCIES: - ExpoBackgroundFetch (from `../../../packages/expo-background-fetch/ios`) - ExpoBackgroundTask (from `../../../packages/expo-background-task/ios`) - ExpoBattery (from `../../../packages/expo-battery/ios`) + - ExpoBlob (from `../../../packages/expo-blob/ios`) - ExpoBlur (from `../../../packages/expo-blur/ios`) - ExpoBrightness (from `../../../packages/expo-brightness/ios`) - ExpoCalendar (from `../../../packages/expo-calendar/ios`) @@ -4044,6 +4112,8 @@ DEPENDENCIES: - ExpoModulesJSI/Tests (from `../../../packages/expo-modules-core`) - ExpoModulesTestCore (from `../../../packages/expo-modules-test-core/ios`) - ExpoNetwork (from `../../../packages/expo-network/ios`) + - ExpoNotifications (from `../../../packages/expo-notifications/ios`) + - ExpoNotifications/Tests (from `../../../packages/expo-notifications/ios`) - ExpoPrint (from `../../../packages/expo-print/ios`) - ExpoRouter (from `../../../packages/expo-router/ios`) - ExpoScreenCapture (from `../../../packages/expo-screen-capture/ios`) @@ -4057,13 +4127,14 @@ DEPENDENCIES: - ExpoStoreReview (from `../../../packages/expo-store-review/ios`) - ExpoSymbols (from `../../../packages/expo-symbols/ios`) - ExpoSystemUI (from `../../../packages/expo-system-ui/ios`) + - ExpoTaskManager (from `../../../packages/expo-task-manager/ios`) - ExpoTrackingTransparency (from `../../../packages/expo-tracking-transparency/ios`) - ExpoVideo (from `../../../packages/expo-video/ios`) - ExpoVideoThumbnails (from `../../../packages/expo-video-thumbnails/ios`) - ExpoWebBrowser (from `../../../packages/expo-web-browser/ios`) + - ExpoWidgets (from `../../../packages/expo-widgets/ios`) - EXStructuredHeaders (from `../../../packages/expo-structured-headers/ios`) - EXStructuredHeaders/Tests (from `../../../packages/expo-structured-headers/ios`) - - EXTaskManager (from `../../../packages/expo-task-manager/ios`) - EXUpdates (from `../../../packages/expo-updates/ios`) - EXUpdates/Tests (from `../../../packages/expo-updates/ios`) - EXUpdatesInterface (from `../../../packages/expo-updates-interface/ios`) @@ -4119,9 +4190,8 @@ DEPENDENCIES: - React-logger (from `../../../react-native-lab/react-native/packages/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../../../react-native-lab/react-native/packages/react-native/ReactCommon`) - React-microtasksnativemodule (from `../../../react-native-lab/react-native/packages/react-native/ReactCommon/react/nativemodule/microtasks`) - - react-native-google-maps (from `../../../node_modules/react-native-maps`) - react-native-keyboard-controller (from `../../../node_modules/react-native-keyboard-controller`) - - react-native-maps (from `../../../node_modules/react-native-maps`) + - react-native-maps/Maps (from `../../../node_modules/react-native-maps`) - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../../../node_modules/react-native-pager-view`) - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) @@ -4189,10 +4259,8 @@ SPEC REPOS: - FirebaseInstallations - FirebaseRemoteConfigInterop - FirebaseSessions - - Google-Maps-iOS-Utils - GoogleAppMeasurement - GoogleDataTransport - - GoogleMaps - GoogleUtilities - libavif - libdav1d @@ -4218,6 +4286,7 @@ SPEC REPOS: - StripePaymentSheet - StripePaymentsUI - StripeUICore + - SwiftUIIntrospect - ZXingObjC EXTERNAL SOURCES: @@ -4229,16 +4298,12 @@ EXTERNAL SOURCES: :path: "../../../packages/expo-eas-client/ios" EXApplication: :path: "../../../packages/expo-application/ios" - EXAV: - :path: "../../../node_modules/expo-av/ios" EXConstants: :path: "../../../packages/expo-constants/ios" EXJSONUtils: :path: "../../../packages/expo-json-utils/ios" EXManifests: :path: "../../../packages/expo-manifests/ios" - EXNotifications: - :path: "../../../packages/expo-notifications/ios" Expo: :path: "../../../packages/expo" expo-dev-menu: @@ -4259,6 +4324,8 @@ EXTERNAL SOURCES: :path: "../../../packages/expo-background-task/ios" ExpoBattery: :path: "../../../packages/expo-battery/ios" + ExpoBlob: + :path: "../../../packages/expo-blob/ios" ExpoBlur: :path: "../../../packages/expo-blur/ios" ExpoBrightness: @@ -4327,6 +4394,8 @@ EXTERNAL SOURCES: :path: "../../../packages/expo-modules-test-core/ios" ExpoNetwork: :path: "../../../packages/expo-network/ios" + ExpoNotifications: + :path: "../../../packages/expo-notifications/ios" ExpoPrint: :path: "../../../packages/expo-print/ios" ExpoRouter: @@ -4353,6 +4422,8 @@ EXTERNAL SOURCES: :path: "../../../packages/expo-symbols/ios" ExpoSystemUI: :path: "../../../packages/expo-system-ui/ios" + ExpoTaskManager: + :path: "../../../packages/expo-task-manager/ios" ExpoTrackingTransparency: :path: "../../../packages/expo-tracking-transparency/ios" ExpoVideo: @@ -4361,10 +4432,10 @@ EXTERNAL SOURCES: :path: "../../../packages/expo-video-thumbnails/ios" ExpoWebBrowser: :path: "../../../packages/expo-web-browser/ios" + ExpoWidgets: + :path: "../../../packages/expo-widgets/ios" EXStructuredHeaders: :path: "../../../packages/expo-structured-headers/ios" - EXTaskManager: - :path: "../../../packages/expo-task-manager/ios" EXUpdates: :path: "../../../packages/expo-updates/ios" EXUpdatesInterface: @@ -4454,8 +4525,6 @@ EXTERNAL SOURCES: :path: "../../../react-native-lab/react-native/packages/react-native/ReactCommon" React-microtasksnativemodule: :path: "../../../react-native-lab/react-native/packages/react-native/ReactCommon/react/nativemodule/microtasks" - react-native-google-maps: - :path: "../../../node_modules/react-native-maps" react-native-keyboard-controller: :path: "../../../node_modules/react-native-keyboard-controller" react-native-maps: @@ -4573,11 +4642,9 @@ SPEC CHECKSUMS: DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EASClient: de38c20c1dd9af7ff435afdc8b2c9b7c20d46767 EXApplication: a9d1c46d473d36f61302a9a81db2379441f3f094 - EXAV: f33ecd2d85522045cb1f0cc2fae3aa53e4e0c3c0 EXConstants: 59d46d25b89f88cc38291a56dbce4d550758f72d EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd EXManifests: 7469efed75d694ce3c43a6da9c6f3886f66d3c26 - EXNotifications: a9eb811deeb51fb8e50ef45569be6ffab3994648 Expo: 446942b564f6fa3f855457d1f1678a9d5c5f3741 expo-dev-menu: ae1feaf51f47590afc4527c6a75165beb7aed0dd expo-dev-menu-interface: 30d9aa2fbca275a1335ca7e076273dac1d42d40a @@ -4588,6 +4655,7 @@ SPEC CHECKSUMS: ExpoBackgroundFetch: e0a8fba22cb21473f667eb8220cab7feef18e63c ExpoBackgroundTask: c6f9ddfb624d9b5ec548d69fd7195745e08ff987 ExpoBattery: da883ecca2a79507916edd00836ef6a50afecac2 + ExpoBlob: e49626a466984cca4ee809628e171041c2d89bc0 ExpoBlur: b5b7a26572b3c33a11f0b2aa2f95c17c4c393b76 ExpoBrightness: 7c3c86da46b847aadf44401bd28fb6d67050f324 ExpoCalendar: 46ad51c48d2cb1a95439c2215b182428919a0c3d @@ -4602,7 +4670,7 @@ SPEC CHECKSUMS: ExpoFileSystem: adffad7d1f57f768ca7a5e3bf450312fb9ebd27a ExpoFont: d3e56c7cc03d9fd113b90a5513ad32b4bf46b0ff ExpoGL: 39262a8c3d4c36d48a28bc412e3392c25c5391c9 - ExpoGlassEffect: 02d9577dfe05a6b6c9856fd71514120e96792052 + ExpoGlassEffect: 00d7b0bc69a33c5fa86c8a107533f6ff1fabff29 ExpoHaptics: b48d913e7e5f23816c6f130e525c9a6501b160b5 ExpoImage: 0fc185a4a4e462fedfe877ae66204a7c541dfee0 ExpoImageManipulator: d6ebf77518412c40d9837d72bf043fd95edec643 @@ -4619,9 +4687,10 @@ SPEC CHECKSUMS: ExpoMediaLibrary: 648cee3f5dcba13410ec9cc8ac9a426e89a61a31 ExpoMeshGradient: 763087d3b1e6e9a0974e9700ea24cb598816d93c ExpoModulesCore: d843eb08a3bc89716cd4eb517f89e17afa451ffc - ExpoModulesJSI: b5e87deda640f9710d08446db5d110e91db64cc9 + ExpoModulesJSI: c470ea2ed825fce73bdc4ef060c8a22e3f664092 ExpoModulesTestCore: e65555b75a4ed7dd3bcf421ad01d7748bd372c88 ExpoNetwork: 97073786edfe405aba5d0987a544617ed0671ad1 + ExpoNotifications: ce045fa5f6ab4109f04468e04e6fe6ff0427a6d1 ExpoPrint: ad836bb90da10793509651c0ea1f39e120db001e ExpoRouter: 84268c2b41722697cb02cd6c377d588b4f71c9b5 ExpoScreenCapture: 0c785ab7e1fa38943f04b16060d54ae8f886544a @@ -4635,12 +4704,13 @@ SPEC CHECKSUMS: ExpoStoreReview: 142af4a031b0a7ad99e70e1de8675da03be4ff40 ExpoSymbols: ef7b8ac77ac2d496b1bc3f0f7daf5e19c3a9933a ExpoSystemUI: 5e7fbe4b716edb9e3a7bc0441e4a3b6b1f9eb1ea + ExpoTaskManager: ea6641e9e4df85753f73d90eb5c3286cb052aaee ExpoTrackingTransparency: 92e731b9b9b09353c3ef48e0fc9f82b89a1957c6 ExpoVideo: 7e39a5933892dc640b1b83013f3f9d9d37072151 ExpoVideoThumbnails: c951ae8c6ed9974dd3ae5f79b2dd1d9b6e8ab266 ExpoWebBrowser: 4f0dc52a559027cc0fba6920adc19a7604e2733d + ExpoWidgets: ce5a271b6480612f0b87123e9ab0e9510cf6fc5e EXStructuredHeaders: c951e77f2d936f88637421e9588c976da5827368 - EXTaskManager: 7d80cb59c1faa3dcfa42035b185bcb5209ca7b1b EXUpdates: a318472f1671f936208c26ef71955415a1b2e279 EXUpdatesInterface: 1436757deb0d574b84bba063bd024c315e0ec08b fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 @@ -4655,12 +4725,10 @@ SPEC CHECKSUMS: FirebaseSessions: f5c6bfeb66a7202deaf33352017bb6365e395820 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321 GoogleAppMeasurement: 8a82b93a6400c8e6551c0bcd66a9177f2e067aed GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 - GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - hermes-engine: 2853aea4922d43f84b721631947e9b25da43eabd + hermes-engine: 452f2dd7422b2fd7973ae9ca103898d28d7744f0 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -4709,17 +4777,16 @@ SPEC CHECKSUMS: React-logger: 6ac901f5c7f7321d2be1a40b203bccc2e23411e3 React-Mapbuffer: 2e0e7cc5b7064eaed9c8b8afc3a87621cb7ef5cd React-microtasksnativemodule: dd4d33b251b57e5027c572c6d0b45cbfbcfaa386 - react-native-google-maps: 7cc1184afe41fbd15a3dffd53c924819f6587b69 - react-native-keyboard-controller: 5e209d13d4c5355ddf8c36be3d41b8dfc879cd91 - react-native-maps: ee1e65647460c3d41e778071be5eda10e3da6225 + react-native-keyboard-controller: d07a35e49d478d26b2eb44b10aedc4e1d35354d6 + react-native-maps: 9cc71a39c6935770047b4a92a35361c3a67c539f react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac - react-native-pager-view: 899989dd025d219d3ac0d386b2bc162f81ae8330 - react-native-safe-area-context: 2249e17382bd5a97cbccaa89e22af8756188c0fb + react-native-pager-view: 4cc305d9d25eda66adf44029a84a591d8b98c072 + react-native-safe-area-context: 54d812805f3c4e08a4580ad086cbde1d8780c2e4 react-native-segmented-control: 44d14c6899ee12de3384517f4fa1cf4a66ae105c - react-native-skia: 6fe142fbe7f93579e6205d4045abdf5d6574b942 - react-native-slider: 30cea7008de785564de2f4fd064f2deb38614a4a + react-native-skia: b3cd83e2bbc7fb7e4e38a23f4ba12fdf0e7e3fbe + react-native-slider: e35799fb9983a19307195d77f10caf5f6a716842 react-native-view-shot: aac285cd08144be29c19ab659930172836f0067f - react-native-webview: 183e0d1f10b3c61c5ddd70f4ecd46488856a3573 + react-native-webview: 8b9097e270a99ee8798449f191a7ea27c790fa1c React-NativeModulesApple: 7f2f2fed3f6c858889eb61d09941be965d52df58 React-networking: 43e5e6773ac2ca2a93261a1388fed269c9fce092 React-oscompat: 80166b66da22e7af7fad94474e9997bd52d4c8c6 @@ -4755,31 +4822,32 @@ SPEC CHECKSUMS: ReactCommon: 05ad684db7d88e194272ae26baddf6300e30b8b7 RNCAsyncStorage: 302f2fac014fd450046c120567ca364632da682b RNCMaskedView: 63268de1986a098b5f4d1fb5b1bc1e97fade0aee - RNCPicker: 51992b3407f6b70ddb8e417ec5714c3f2d48d8a4 - RNDateTimePicker: c4a42c6a77d474f05162d61a4ab4253d13008277 - RNGestureHandler: afae32614741017de0f813f74586eee29773f31a - RNReanimated: ef851cdf95ca947de3199fb05b863be9596a9a91 + RNCPicker: 3fc4d505041318ee8f59662d8d66892ce3c3295c + RNDateTimePicker: e9e210197c267461f70f3f47bec705401ff72077 + RNGestureHandler: 77eecab5fd636666ca73a55bb61e2f1a685b7e84 + RNReanimated: 31da8d5f1605f5367e2392748ba9f4ba6eaf1178 RNScreens: 69f68c95d395bc4261d27c3aae7b4a458b947b7e - RNSVG: 6e742388ed2c7bfe7c9f5baf42b8eb850adc0442 + RNSVG: 55fc5dc0eaa36a875ffb7d05c0f2cd5b2cbfc342 RNWorklets: 4e3230b74c2e466e608458b7f665a41825bca6c1 SDWebImage: f29024626962457f3470184232766516dee8dfea SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Stripe: d1824162e4bcb6ead10401a0dc8e8a3d5ffee41f - stripe-react-native: fd628fd2cd45a6d06700cdf499f23dbcb4d5f8d9 - StripeApplePay: eb64705d3c919492b1b28876652fd0aca2db38cc - StripeCore: b5bee05167f0c8ccce936244a031a9972fdeade8 - StripeFinancialConnections: fd6c661f86f47b1d26a32f588b3a0b09826aba84 - StripePayments: cf00b3c85cd7b57e40f622493f81bdf361cb3982 - StripePaymentSheet: b126816213ae4f56ff4525ad25f94bb985a43e27 - StripePaymentsUI: 05d4be35b34f821d5ddc8617dee04219f3646253 - StripeUICore: d07bd63a4b53dee8cab4d6f81ce5a7ab3e28511e - UMAppLoader: e1234c45d2b7da239e9e90fc4bbeacee12afd5b6 + Stripe: 4728e3e0dd8df134e4a420ab504e929a93a815f0 + stripe-react-native: 3fde1e37716139e55b3131de08dc1d211aafe38f + StripeApplePay: 43997281ace138a1c75a8f2d7be11925ea28644c + StripeCore: 457c30e2fd3a7c4b274a5ad53d1ff03661eef2a0 + StripeFinancialConnections: 8c2e326f767fb014b53174b3a5f8592c0a45fa56 + StripePayments: 6955de4298a5265e66f02cffcc7954475ac7f6c8 + StripePaymentSheet: 3f93ce6ea84afde770d3c7e18a9b8f99aed63896 + StripePaymentsUI: 626726a01255a6458c35436f7f6431dacee82684 + StripeUICore: 30f8352fd7a5cf1541b7777a57b3ad1133bf6763 + SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d + UMAppLoader: b649e542381c8abe788ffb13893e632d598910a5 Yoga: 5456bb010373068fc92221140921b09d126b116e ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: 4cc10e62a4c97b54a0234169440b8fb43688a74f +PODFILE CHECKSUM: c85608cb6a639c43a2843bc635b0aface4b69702 COCOAPODS: 1.16.2 diff --git a/apps/expo-go/ios/Tests/AppLoader/EXAppLoader+Tests.h b/apps/expo-go/ios/Tests/AppLoader/EXAppLoader+Tests.h deleted file mode 100644 index 5eb31392049c4c..00000000000000 --- a/apps/expo-go/ios/Tests/AppLoader/EXAppLoader+Tests.h +++ /dev/null @@ -1,13 +0,0 @@ -#import "EXDevelopmentHomeLoader.h" - -@class EXManifestsManifest; - -#pragma mark - private/internal methods in App Loader & App Fetchers - -@interface EXDevelopmentHomeLoader (EXAppLoaderTests) - -@property (nonatomic, readonly) EXAppFetcher * _Nullable appFetcher; - -- (BOOL)_fetchBundleWithManifest:(EXManifestsManifest *)manifest; - -@end diff --git a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderConfigurationTests.m b/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderConfigurationTests.m deleted file mode 100644 index 1e3fe26b68a4b6..00000000000000 --- a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderConfigurationTests.m +++ /dev/null @@ -1,65 +0,0 @@ -#import - -#import "EXAppLoader+Tests.h" -#import "EXAppFetcherWithTimeout.h" -#import "EXClientTestCase.h" -#import "EXEnvironment.h" - -@interface EXAppFetcherWithTimeout (EXAppLoaderTests) - -@property (nonatomic, readonly) NSTimeInterval timeout; - -@end - -@interface EXAppLoaderConfigurationTests : EXClientTestCase - -@end - -@implementation EXAppLoaderConfigurationTests - -- (void)setUp -{ - [super setUp]; - - if ([EXEnvironment sharedEnvironment].testEnvironment == EXTestEnvironmentNone) { - [EXEnvironment sharedEnvironment].testEnvironment = EXTestEnvironmentLocal; - } -} - -#pragma mark - app loader configuration in app.json - -- (void)testIsDefaultUpdatesConfigUsed -{ - NSDictionary *manifest = @{}; - EXDevelopmentHomeLoader *appLoader = [[EXDevelopmentHomeLoader alloc] initWithManifestUrl:[NSURL URLWithString:@"exp://exp.host/@esamelson/test-fetch-update"]]; - [appLoader _fetchBundleWithManifest:manifest]; - XCTAssert([appLoader.appFetcher isKindOfClass:[EXAppFetcherWithTimeout class]], @"AppLoader should choose to use AppFetcherWithTimeout when fetching remotely"); - XCTAssert([(EXAppFetcherWithTimeout *)appLoader.appFetcher timeout] == kEXAppLoaderDefaultTimeout, @"AppFetcherWithTimeout should have the correct user-specified timeout length"); -} - -- (void)testIsUpdateTimeoutConfigRespected -{ - NSDictionary *manifest = @{ - @"updates": @{ - @"fallbackToCacheTimeout": @1000, - } - }; - EXDevelopmentHomeLoader *appLoader = [[EXDevelopmentHomeLoader alloc] initWithManifestUrl:[NSURL URLWithString:@"exp://exp.host/@esamelson/test-fetch-update"]]; - [appLoader _fetchBundleWithManifest:manifest]; - XCTAssert([appLoader.appFetcher isKindOfClass:[EXAppFetcherWithTimeout class]], @"AppLoader should choose to use AppFetcherWithTimeout when fetching remotely"); - XCTAssert([(EXAppFetcherWithTimeout *)appLoader.appFetcher timeout] == 1.0f, @"AppFetcherWithTimeout should have the correct user-specified timeout length"); -} - -- (void)testIsOnErrorRecoveryIgnoredInExpoClient -{ - NSDictionary *manifest = @{ - @"updates": @{ - @"checkAutomatically": @"ON_ERROR_RECOVERY" - } - }; - EXDevelopmentHomeLoader *appLoader = [[EXDevelopmentHomeLoader alloc] initWithManifestUrl:[NSURL URLWithString:@"exp://exp.host/@esamelson/test-fetch-update"]]; - [appLoader _fetchBundleWithManifest:manifest]; - XCTAssert([appLoader.appFetcher isKindOfClass:[EXAppFetcherWithTimeout class]], @"AppLoader should ignore ON_ERROR_RECOVERY in Expo Go"); -} - -@end diff --git a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestExpectation.h b/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestExpectation.h deleted file mode 100644 index 7e445f8a5f27e2..00000000000000 --- a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestExpectation.h +++ /dev/null @@ -1,16 +0,0 @@ -// this class builds an AppLoader, starts a request, -// then waits for the AppLoader to succeed or fail -// and reports the result to the corresponding XCTestExpectation. - -#import - -@interface EXAppLoaderRequestExpectation : NSObject - -- (instancetype)initWithUrl:(NSURL *)urlToRequest - expectToSucceed:(XCTestExpectation *)expectToSucceed - expectToFail:(XCTestExpectation *)expectToFail; -- (void)request; - -@property (nonatomic, readonly) EXDevelopmentHomeLoader *appLoader; - -@end diff --git a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestExpectation.m b/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestExpectation.m deleted file mode 100644 index df84289d798b73..00000000000000 --- a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestExpectation.m +++ /dev/null @@ -1,62 +0,0 @@ - -#import - -#import "EXAppLoader+Tests.h" -#import "EXAppLoaderRequestExpectation.h" - -@interface EXAppLoaderRequestExpectation () - -@property (nonatomic, strong) NSURL *url; -@property (nonatomic, strong) XCTestExpectation *expectToSucceed; -@property (nonatomic, strong) XCTestExpectation *expectToFail; -@property (nonatomic, strong) EXDevelopmentHomeLoader *appLoader; - -@end - -@implementation EXAppLoaderRequestExpectation - -- (instancetype)initWithUrl:(NSURL *)url expectToSucceed:(XCTestExpectation *)expectToSucceed expectToFail:(XCTestExpectation *)expectToFail -{ - if (self = [super init]) { - _url = url; - _expectToSucceed = expectToSucceed; - _expectToFail = expectToFail; - _appLoader = [[EXDevelopmentHomeLoader alloc] initWithManifestUrl:_url]; - _appLoader.delegate = self; - } - return self; -} - -- (void)request -{ - [_appLoader request]; -} - -#pragma mark - AppLoaderDelegate - -- (void)appLoader:(EXDevelopmentHomeLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest -{ - -} - -- (void)appLoader:(EXDevelopmentHomeLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress -{ - -} - -- (void)appLoader:(EXDevelopmentHomeLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data -{ - [_expectToSucceed fulfill]; -} - -- (void)appLoader:(EXDevelopmentHomeLoader *)appLoader didFailWithError:(NSError *)error -{ - [_expectToFail fulfill]; -} - -- (void)appLoader:(EXDevelopmentHomeLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error -{ - -} - -@end diff --git a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestTests.m b/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestTests.m deleted file mode 100644 index 7b6d2f5b9f1831..00000000000000 --- a/apps/expo-go/ios/Tests/AppLoader/EXAppLoaderRequestTests.m +++ /dev/null @@ -1,70 +0,0 @@ - -#import -#import "EXAppLoader+Tests.h" -#import "EXAppLoaderRequestExpectation.h" - -@interface EXAppLoaderRequestTests : XCTestCase - -@end - -@implementation EXAppLoaderRequestTests - -#pragma mark - requests that should succeed - -- (void)testDoesNCLLoad -{ - [self _testDoesUrlLoadSuccessfully:[NSURL URLWithString:@"exp://exp.host/@community/native-component-list"] - description:@"AppLoader should load something for Native Component List"]; -} - -#pragma mark - requests that should fail - -- (void)testDoesUnsupportedSdkFailToLoad -{ - [self _testDoesUrlFailToLoad:[NSURL URLWithString:@"exp://exp.host/@ben/test-sdk20"] - description:@"AppLoader should not load a valid app running an unsupported SDK version"]; -} - -- (void)testDoesUnsupportedReleaseChannelFailToLoad -{ - [self _testDoesUrlFailToLoad:[NSURL URLWithString:@"exp://exp.host/@ben/test-sdk28?release-channel=fake"] - description:@"AppLoader should not load anything from a valid app with a nonexistent release channel"]; -} - -- (void)testDoesInvalidUrlFailToLoad -{ - [self _testDoesUrlFailToLoad:[NSURL URLWithString:@"exp://abcdef"] - description:@"AppLoader should not load anything from a nonexistent app url"]; -} - -#pragma mark - internal - -- (void)_testDoesUrlFailToLoad:(NSURL *)url description:(NSString *)description -{ - XCTestExpectation *expectToSucceed = [[XCTestExpectation alloc] initWithDescription:description]; - [expectToSucceed setInverted:YES]; // this test should fail if the request succeeds in loading an app. - XCTestExpectation *expectToFail = [[XCTestExpectation alloc] initWithDescription:description]; - EXAppLoaderRequestExpectation *test = [[EXAppLoaderRequestExpectation alloc] initWithUrl:url expectToSucceed:expectToSucceed expectToFail:expectToFail]; - [test request]; - - // wait for request to fail - // this will take the full 10 seconds because it also enforces that `expectToSucceed` times out - [self waitForExpectations:@[ expectToSucceed, expectToFail ] timeout:10.0f]; - - // perform additional validation on AppLoader - XCTAssert(test.appLoader.appFetcher != nil, @"App loader should preserve app fetcher state after failure"); -} - -- (void)_testDoesUrlLoadSuccessfully:(NSURL *)url description:(NSString *)description -{ - XCTestExpectation *expectToSucceed = [[XCTestExpectation alloc] initWithDescription:description]; - XCTestExpectation *expectToFail = [[XCTestExpectation alloc] initWithDescription:description]; - [expectToFail setInverted:YES]; // this test should fail if the request calls its failure handler. - EXAppLoaderRequestExpectation *test = [[EXAppLoaderRequestExpectation alloc] initWithUrl:url expectToSucceed:expectToSucceed expectToFail:expectToFail]; - [test request]; - - // wait for request to load - [self waitForExpectations:@[ expectToSucceed ] timeout:30.0f]; -} - -@end diff --git a/apps/expo-go/ios/Tests/AppLoader/EXFileDownloaderTests.m b/apps/expo-go/ios/Tests/AppLoader/EXFileDownloaderTests.m deleted file mode 100644 index 8863df1372918f..00000000000000 --- a/apps/expo-go/ios/Tests/AppLoader/EXFileDownloaderTests.m +++ /dev/null @@ -1,68 +0,0 @@ -#import - -#import "EXClientTestCase.h" -#import "EXEnvironment.h" -#import "EXFileDownloader.h" - -@interface EXFileDownloaderTests : EXClientTestCase - -@end - -@implementation EXFileDownloaderTests - -- (void)setUp -{ - [super setUp]; - - if ([EXEnvironment sharedEnvironment].testEnvironment == EXTestEnvironmentNone) { - [EXEnvironment sharedEnvironment].testEnvironment = EXTestEnvironmentLocal; - } -} - -#pragma mark - file downloader - -- (void)testIsExpoSDKVersionHeaderConfigured -{ - NSURLRequest *request = [self _mockJsBundleDownloadRequest]; - NSString *sdkVersionHeader = [request valueForHTTPHeaderField:@"Exponent-SDK-Version"]; - NSArray *sdkVersions = [sdkVersionHeader componentsSeparatedByString:@","]; - XCTAssert(sdkVersions.count > 0, @"Expo SDK version header should contain at least one comma-separated SDK version"); -} - -- (void)testAreOtherHeadersConfigured -{ - NSURLRequest *request = [self _mockJsBundleDownloadRequest]; - NSArray *requiredHeaderFields = @[ - @"Exponent-SDK-Version", - @"Exponent-Platform", - @"Exponent-Accept-Signature", - ]; - for (NSString *header in requiredHeaderFields) { - NSString *headerValue = [request valueForHTTPHeaderField:header]; - XCTAssert((headerValue != nil), @"HTTP header %@ should be set", header); - } -} - -- (void)testDoesDefaultFileDownloaderDownloadSomething -{ - XCTestExpectation *expectToDownload = [[XCTestExpectation alloc] initWithDescription:@"Default EXFileDownloader should download a json file"]; - EXFileDownloader *fileDownloader = [[EXFileDownloader alloc] init]; - NSURL *jsonFileUrl = [NSURL URLWithString:@"https://expo.io/@exponent/home/index.exp"]; - [fileDownloader downloadFileFromURL:jsonFileUrl successBlock:^(NSData * _Nonnull data, NSURLResponse * _Nonnull response) { - [expectToDownload fulfill]; - } errorBlock:^(NSError * _Nonnull error, NSURLResponse * _Nonnull response) {}]; - [self waitForExpectations:@[ expectToDownload ] timeout:10.0]; -} - -#pragma mark - internal - -- (NSMutableURLRequest *)_mockJsBundleDownloadRequest -{ - // mock a url request for a JS bundle - NSMutableURLRequest *jsBundleDownloadRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://exp.host/@exponent/home/bundle"]]; - EXFileDownloader *downloader = [[EXFileDownloader alloc] init]; - [downloader setHTTPHeaderFields:jsBundleDownloadRequest]; - return jsBundleDownloadRequest; -} - -@end diff --git a/apps/expo-go/ios/Tests/Environment/EXClientTestCase.h b/apps/expo-go/ios/Tests/Environment/EXClientTestCase.h deleted file mode 100644 index c2af7b0c4b6ca7..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXClientTestCase.h +++ /dev/null @@ -1,7 +0,0 @@ -// test cases which should pass when the runtime is running in Expo Go. - -#import - -@interface EXClientTestCase : XCTestCase - -@end diff --git a/apps/expo-go/ios/Tests/Environment/EXClientTestCase.m b/apps/expo-go/ios/Tests/Environment/EXClientTestCase.m deleted file mode 100644 index 02c9a7e25cdd36..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXClientTestCase.m +++ /dev/null @@ -1,13 +0,0 @@ - -#import "EXClientTestCase.h" -#import "EXEnvironmentMocks.h" - -@implementation EXClientTestCase - -- (void)setUp -{ - [super setUp]; - [EXEnvironmentMocks loadExpoClientConfig]; -} - -@end diff --git a/apps/expo-go/ios/Tests/Environment/EXDevTestCase.h b/apps/expo-go/ios/Tests/Environment/EXDevTestCase.h deleted file mode 100644 index 59bb63b4bdc735..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXDevTestCase.h +++ /dev/null @@ -1,7 +0,0 @@ -// test cases which should pass when the runtime is running in a dev (local) environment. - -#import - -@interface EXDevTestCase : XCTestCase - -@end diff --git a/apps/expo-go/ios/Tests/Environment/EXDevTestCase.m b/apps/expo-go/ios/Tests/Environment/EXDevTestCase.m deleted file mode 100644 index 2d1a26e973350c..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXDevTestCase.m +++ /dev/null @@ -1,13 +0,0 @@ - -#import "EXDevTestCase.h" -#import "EXEnvironmentMocks.h" - -@implementation EXDevTestCase - -- (void)setUp -{ - [super setUp]; - [EXEnvironmentMocks loadDevConfig]; -} - -@end diff --git a/apps/expo-go/ios/Tests/Environment/EXEnvironment+Tests.h b/apps/expo-go/ios/Tests/Environment/EXEnvironment+Tests.h deleted file mode 100644 index 7f1520af3e95b1..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXEnvironment+Tests.h +++ /dev/null @@ -1,9 +0,0 @@ -#import "EXEnvironment.h" -#import - -@interface EXEnvironment (Tests) - -- (void)_resetAndLoadIsDebugXCodeScheme:(BOOL)isDebugScheme; -- (void)_loadDefaultConfig; - -@end diff --git a/apps/expo-go/ios/Tests/Environment/EXEnvironmentMocks.h b/apps/expo-go/ios/Tests/Environment/EXEnvironmentMocks.h deleted file mode 100644 index 788432d720af20..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXEnvironmentMocks.h +++ /dev/null @@ -1,15 +0,0 @@ -#import - -@interface EXEnvironmentMocks : NSObject - -/** - * Load mock configuration for Expo Go. - */ -+ (void)loadExpoClientConfig; - -/** - * Load mock configuration for native component list as dev detached xdl should write it. - */ -+ (void)loadDevConfig; - -@end diff --git a/apps/expo-go/ios/Tests/Environment/EXEnvironmentMocks.m b/apps/expo-go/ios/Tests/Environment/EXEnvironmentMocks.m deleted file mode 100644 index 174c094353a56e..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXEnvironmentMocks.m +++ /dev/null @@ -1,19 +0,0 @@ -#import "EXEnvironment.h" -#import "EXEnvironment+Tests.h" -#import "EXEnvironmentMocks.h" - -@implementation EXEnvironmentMocks - -#pragma mark - mock environment presets - -+ (void)loadExpoClientConfig -{ - [[EXEnvironment sharedEnvironment] _resetAndLoadIsDebugXCodeScheme:NO]; -} - -+ (void)loadDevConfig -{ - [[EXEnvironment sharedEnvironment] _resetAndLoadIsDebugXCodeScheme:YES]; -} - - @end diff --git a/apps/expo-go/ios/Tests/Environment/EXEnvironmentTests.m b/apps/expo-go/ios/Tests/Environment/EXEnvironmentTests.m deleted file mode 100644 index 8f3cbf9b210f82..00000000000000 --- a/apps/expo-go/ios/Tests/Environment/EXEnvironmentTests.m +++ /dev/null @@ -1,36 +0,0 @@ -#import "EXEnvironment.h" -#import "EXEnvironment+Tests.h" -#import "EXEnvironmentMocks.h" - -#import - -@interface EXEnvironmentTests : XCTestCase - -@property (nonatomic, weak) EXEnvironment *environment; - -@end - -@implementation EXEnvironmentTests - -- (void)setUp -{ - [super setUp]; - - _environment = [EXEnvironment sharedEnvironment]; - if (_environment.testEnvironment == EXTestEnvironmentNone) { - _environment.testEnvironment = EXTestEnvironmentLocal; - } -} - -- (void)testDoesDefaultConfigRespectXcodeScheme -{ - [_environment _loadDefaultConfig]; -#if DEBUG - XCTAssert(_environment.isDebugXCodeScheme, @"Default EXEnvironment config should respect Debug Xcode scheme"); -#else - XCTAssert(!(_environment.isDebugXCodeScheme), @"Default EXEnvironment config should respect Release Xcode scheme"); -#endif -} - -@end - diff --git a/apps/expo-go/ios/Tests/Info.plist b/apps/expo-go/ios/Tests/Info.plist deleted file mode 100644 index 6c40a6cd0c4af2..00000000000000 --- a/apps/expo-go/ios/Tests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/apps/expo-go/ios/Tests/Linking/EXLinkingTests.m b/apps/expo-go/ios/Tests/Linking/EXLinkingTests.m deleted file mode 100644 index c1d792d8bbc46b..00000000000000 --- a/apps/expo-go/ios/Tests/Linking/EXLinkingTests.m +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Tests deep link transforming/routing methods in the Expo runtime. - */ - -#import -#import "EXKernel.h" -#import "EXKernelLinkingManager.h" - -#pragma mark - private/internal methods in Linking Manager - -@interface EXKernelLinkingManager (EXLinkingTests) - -+ (BOOL)_isUrl:(NSURL *)urlToRoute deepLinkIntoAppWithManifestUrl:(NSURL *)manifestUrl; - -@end - -@interface EXLinkingTests : XCTestCase - -@end - -@implementation EXLinkingTests - -- (void)setUp -{ - [super setUp]; -} - -#pragma mark - test link routing - -- (void)testIsClientDeepLinkRoutedCorrectly -{ - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin" routesToManifest:@"https://exp.host/@ben/foodwheel"]; -} - -- (void)testIsDeepLinkingInvariantToExpoSchemes -{ - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"http://exp.host/@ben/foodwheel/--/spin" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"exp://exp.host/@ben/foodwheel/--/spin" routesToManifest:@"https://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"exps://exp.host/@ben/foodwheel/--/spin" routesToManifest:@"https://exp.host/@ben/foodwheel"]; -} - -- (void)testIsDeepLinkingInvariantToReleaseChannelDefault -{ - // deep link contains release channel default, manifest does not specify - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?release-channel=default" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel?release-channel=default" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/?release-channel=default" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - - // deep link does not specify a release channel, manifest specifies default - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel" routesToManifest:@"exp://exp.host/@ben/foodwheel?release-channel=default"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin" routesToManifest:@"exp://exp.host/@ben/foodwheel?release-channel=default"]; - - // deep link and manifest both specify default release channel - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?release-channel=default" routesToManifest:@"exp://exp.host/@ben/foodwheel?release-channel=default"]; -} - -- (void)testDoesDeepLinkingRespectCustomReleaseChannel -{ - // deep link specifies custom release channel, manifest does not specify, or uses default - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel?release-channel=banana" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?release-channel=banana" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel?release-channel=banana" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel?release-channel=default"]; - - // deep link does not specify a release channel, or uses default; manifest specifies custom - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel?release-channel=banana"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel?release-channel=banana"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?release-channel=default" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel?release-channel=banana"]; -} - -- (void)testDoesDeepLinkingDifferentiateDomains -{ - [self _assertDeepLink:@"https://expo.io/@ben/foodwheel" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://google.com/@ben/foodwheel" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://@ben/foodwheel" doesNotRouteToManifest:@"https://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://expo.io/@ben/foodwheel" doesNotRouteToManifest:@"https://@ben/foodwheel"]; -} - -- (void)testIsDeepLinkingInvariantToQueryString -{ - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel?a=b&c=d" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel?a=b&c=d" routesToManifest:@"exp://exp.host/@ben/foodwheel?release-channel=default"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel?a=b&c=d&release-channel=default" routesToManifest:@"exp://exp.host/@ben/foodwheel?release-channel=default"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?a=b&c=d" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?a=b&c=d" routesToManifest:@"exp://exp.host/@ben/foodwheel?release-channel=default"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?a=b&c=d&release-channel=default" routesToManifest:@"exp://exp.host/@ben/foodwheel?release-channel=default"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/spin?a=b&c=d&release-channel=banana" doesNotRouteToManifest:@"exp://exp.host/@ben/foodwheel"]; -} - -- (void)testIsDeepLinkingInvariantToDeepLinkPath -{ - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel/--/a/b/c" routesToManifest:@"exp://exp.host/@ben/foodwheel"]; - [self _assertDeepLink:@"https://exp.host/@ben/foodwheel" routesToManifest:@"exp://exp.host/@ben/foodwheel/--/a/b/c"]; -} - -- (void)testArePhoneLinksNotDeepLinks -{ - // tel and sms links should never be deep links - NSURL *smsUrl = [NSURL URLWithString:@"sms:+604-288-8555"]; - NSURL *telUrl = [NSURL URLWithString:@"tel:1-408-555-5555"]; - for (NSURL *url in @[ smsUrl, telUrl ]) { - XCTAssert([[EXKernel sharedInstance].serviceRegistry.linkingManager linkingModule:nil shouldOpenExpoUrl:url] == NO, - @"URL %@ should not be routed internally as a deep link", url); - } -} - -- (void)testAreExpoSubdomainsNotDeepLinks -{ - NSURL *docsUrl = [NSURL URLWithString:@"https://docs.expo.io"]; - XCTAssert([[EXKernel sharedInstance].serviceRegistry.linkingManager linkingModule:nil shouldOpenExpoUrl:docsUrl] == NO, - @"URL %@ should not be routed internally as a deep link", docsUrl); -} - -#pragma mark - test url parsing/transforms - -- (void)testIsDeepLinkRemoved -{ - NSString *manifestWithNoDeepLink = @"https://exp.host/@ben/foodwheel/"; - NSArray *deepLinks = @[ - @"https://exp.host/@ben/foodwheel/--/", - @"https://exp.host/@ben/foodwheel/--/spin", - @"https://exp.host/@ben/foodwheel/--/a/b/c", - @"https://exp.host/@ben/foodwheel/--/spin?a=b&c=d", - // @"https://exp.host/@ben/foodwheel?a=b&c=d", // TODO: should this case be supported? - ]; - for (NSString *deepLink in deepLinks) { - NSString *result = [EXKernelLinkingManager stringByRemovingDeepLink:deepLink]; - XCTAssert([result isEqualToString:manifestWithNoDeepLink], - @"Linking manager should correctly remove the deep link from %@, but instead it returned %@", deepLink, result); - } -} - -#pragma mark - EAS manifests - -- (void)testEASManifestUrls { - [self _assertDeepLink:@"exps://updates.expo.dev/37700852-0840-47b7-80cb-d57746395f57?runtime-version=exposdk%3A40.0.0&channel-name=main" routesToManifest:@"exps://updates.expo.dev/37700852-0840-47b7-80cb-d57746395f57?runtime-version=exposdk%3A40.0.0&channel-name=main"]; -} - -#pragma mark - internal - -- (void)_assertDeepLink:(NSString *)deepLinkUrlString routesToManifest:(NSString *)manifestUrlString -{ - NSURL *deepLinkUrl = [NSURL URLWithString:deepLinkUrlString]; - NSURL *manifestUrl = [NSURL URLWithString:manifestUrlString]; - BOOL result = [EXKernelLinkingManager _isUrl:deepLinkUrl deepLinkIntoAppWithManifestUrl:manifestUrl]; - XCTAssert(result, @"Url %@ should deep link to manifest url %@", deepLinkUrl, manifestUrl); -} - -- (void)_assertDeepLink:(NSString *)deepLinkUrlString doesNotRouteToManifest:(NSString *)manifestUrlString -{ - NSURL *deepLinkUrl = [NSURL URLWithString:deepLinkUrlString]; - NSURL *manifestUrl = [NSURL URLWithString:manifestUrlString]; - BOOL result = [EXKernelLinkingManager _isUrl:deepLinkUrl deepLinkIntoAppWithManifestUrl:manifestUrl]; - XCTAssert(!result, @"Url %@ should NOT deep link to manifest url %@", deepLinkUrl, manifestUrl); -} - -@end diff --git a/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java index 5932fc189d192c..80c68034b193a4 100644 --- a/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +++ b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java @@ -91,10 +91,6 @@ public RNCWebView(ThemedReactContext reactContext) { progressChangedFilter = new ProgressChangedFilter(); } - public void setIgnoreErrFailedForThisURL(String url) { - mRNCWebViewClient.setIgnoreErrFailedForThisURL(url); - } - public void setBasicAuthCredential(RNCBasicAuthCredential credential) { mRNCWebViewClient.setBasicAuthCredential(credential); } diff --git a/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java index 251939e56ac248..0287e1c56b07b6 100644 --- a/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java +++ b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewClient.java @@ -25,6 +25,7 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIManagerHelper; +import com.reactnativecommunity.webview.events.SubResourceErrorEvent; import com.reactnativecommunity.webview.events.TopHttpErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingFinishEvent; @@ -42,13 +43,8 @@ public class RNCWebViewClient extends WebViewClient { protected boolean mLastLoadFailed = false; protected RNCWebView.ProgressChangedFilter progressChangedFilter = null; - protected @Nullable String ignoreErrFailedForThisURL = null; protected @Nullable RNCBasicAuthCredential basicAuthCredential = null; - public void setIgnoreErrFailedForThisURL(@Nullable String url) { - ignoreErrFailedForThisURL = url; - } - public void setBasicAuthCredential(@Nullable RNCBasicAuthCredential credential) { basicAuthCredential = credential; } @@ -173,12 +169,6 @@ public void onReceivedSslError(final WebView webView, final SslErrorHandler hand // Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL. handler.cancel(); - if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { - // If error is not due to top-level navigation, then do not call onReceivedError() - Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); - return; - } - int code = error.getPrimaryError(); String description = ""; String descriptionPrefix = "SSL error: "; @@ -210,6 +200,18 @@ public void onReceivedSslError(final WebView webView, final SslErrorHandler hand description = descriptionPrefix + description; + if (!topWindowUrl.equalsIgnoreCase(failingUrl)) { + // If error is not due to top-level navigation, then do not call onReceivedError() + Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl); + this.onReceivedSubResourceSslError( + webView, + code, + description, + failingUrl + ); + return; + } + this.onReceivedError( webView, code, @@ -218,6 +220,20 @@ public void onReceivedSslError(final WebView webView, final SslErrorHandler hand ); } + public void onReceivedSubResourceSslError( + WebView webView, + int errorCode, + String description, + String failingUrl) { + + WritableMap eventData = createWebViewEvent(webView, failingUrl); + eventData.putDouble("code", errorCode); + eventData.putString("description", description); + + int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView); + UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new SubResourceErrorEvent(reactTag, eventData)); + } + @Override public void onReceivedError( WebView webView, @@ -225,20 +241,6 @@ public void onReceivedError( String description, String failingUrl) { - if (ignoreErrFailedForThisURL != null - && failingUrl.equals(ignoreErrFailedForThisURL) - && errorCode == -1 - && description.equals("net::ERR_FAILED")) { - - // This is a workaround for a bug in the WebView. - // See these chromium issues for more context: - // https://bugs.chromium.org/p/chromium/issues/detail?id=1023678 - // https://bugs.chromium.org/p/chromium/issues/detail?id=1050635 - // This entire commit should be reverted once this bug is resolved in chromium. - setIgnoreErrFailedForThisURL(null); - return; - } - super.onReceivedError(webView, errorCode, description, failingUrl); mLastLoadFailed = true; diff --git a/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt index 438ab6a659930d..f83033b3597a42 100644 --- a/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +++ b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt @@ -92,7 +92,6 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) { WebView.setWebContentsDebuggingEnabled(true) } webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> - webView.setIgnoreErrFailedForThisURL(url) val module = webView.reactApplicationContext.getNativeModule(RNCWebViewModule::class.java) ?: return@DownloadListener val request: DownloadManager.Request = try { DownloadManager.Request(Uri.parse(url)) diff --git a/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt new file mode 100644 index 00000000000000..24ac2f31911ffd --- /dev/null +++ b/apps/expo-go/modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/events/SubResourceErrorEvent.kt @@ -0,0 +1,25 @@ +package com.reactnativecommunity.webview.events + +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +/** + * Event emitted when there is an error in loading a subresource + */ +class SubResourceErrorEvent(viewId: Int, private val mEventData: WritableMap) : + Event(viewId) { + companion object { + const val EVENT_NAME = "topLoadingSubResourceError" + } + + override fun getEventName(): String = EVENT_NAME + + override fun canCoalesce(): Boolean = false + + override fun getCoalescingKey(): Short = 0 + + override fun dispatch(rctEventEmitter: RCTEventEmitter) = + rctEventEmitter.receiveEvent(viewTag, eventName, mEventData) + +} diff --git a/apps/expo-go/modules/react-native-webview/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/apps/expo-go/modules/react-native-webview/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java index e20ae03bf1187f..4709e282249906 100644 --- a/apps/expo-go/modules/react-native-webview/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/apps/expo-go/modules/react-native-webview/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -15,6 +15,7 @@ import com.facebook.react.viewmanagers.RNCWebViewManagerInterface; import com.facebook.react.views.scroll.ScrollEventType; import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent; +import com.reactnativecommunity.webview.events.SubResourceErrorEvent; import com.reactnativecommunity.webview.events.TopHttpErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingFinishEvent; @@ -524,6 +525,7 @@ public Map getExportedCustomDirectEventTypeConstants() { export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart")); export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish")); export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError")); + export.put(SubResourceErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingSubResourceError")); export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage")); // !Default events but adding them here explicitly for clarity diff --git a/apps/expo-go/modules/react-native-webview/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/apps/expo-go/modules/react-native-webview/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 44d33783b415e2..974337f41ce01a 100644 --- a/apps/expo-go/modules/react-native-webview/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/apps/expo-go/modules/react-native-webview/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -11,6 +11,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.views.scroll.ScrollEventType; import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent; +import com.reactnativecommunity.webview.events.SubResourceErrorEvent; import com.reactnativecommunity.webview.events.TopHttpErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingErrorEvent; import com.reactnativecommunity.webview.events.TopLoadingFinishEvent; @@ -294,6 +295,7 @@ public Map getExportedCustomDirectEventTypeConstants() { export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart")); export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish")); export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError")); + export.put(SubResourceErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingSubResourceError")); export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage")); // !Default events but adding them here explicitly for clarity diff --git a/apps/expo-go/modules/react-native-webview/apple/RNCWebView.h b/apps/expo-go/modules/react-native-webview/apple/RNCWebView.h index 659334e215b522..ad0b778d74932a 100644 --- a/apps/expo-go/modules/react-native-webview/apple/RNCWebView.h +++ b/apps/expo-go/modules/react-native-webview/apple/RNCWebView.h @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN namespace facebook { namespace react { - bool operator==(const RNCWebViewMenuItemsStruct& a, const RNCWebViewMenuItemsStruct& b) + inline bool operator==(const RNCWebViewMenuItemsStruct& a, const RNCWebViewMenuItemsStruct& b) { return b.key == a.key && b.label == a.label; } diff --git a/apps/expo-go/modules/react-native-webview/index.d.ts b/apps/expo-go/modules/react-native-webview/index.d.ts index a85ac85357b412..a3eef1390c51d4 100644 --- a/apps/expo-go/modules/react-native-webview/index.d.ts +++ b/apps/expo-go/modules/react-native-webview/index.d.ts @@ -51,7 +51,7 @@ declare class WebView

extends Component { /** * Clears the resource cache. Note that the cache is per-application, so this will clear the cache for all WebViews used. */ - clearCache?: (includeDiskFiles: boolean) => void; + clearCache: (includeDiskFiles: boolean) => void; /** * (Android only) diff --git a/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.d.ts b/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.d.ts index d0de585c535380..8dd6cef93c86f3 100644 --- a/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.d.ts +++ b/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.d.ts @@ -199,6 +199,7 @@ export interface NativeProps extends ViewProps { mediaPlaybackRequiresUserAction?: WithDefault; messagingEnabled: boolean; onLoadingError: DirectEventHandler; + onLoadingSubResourceError: DirectEventHandler; onLoadingFinish: DirectEventHandler; onLoadingProgress: DirectEventHandler; onLoadingStart: DirectEventHandler; diff --git a/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.js b/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.js index 5a6606a01363b5..25d09e9e702ce7 100644 --- a/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.js +++ b/apps/expo-go/modules/react-native-webview/lib/RNCWebViewNativeComponent.js @@ -1 +1 @@ -var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.default=exports.__INTERNAL_VIEW_CONFIG=exports.Commands=void 0;var _codegenNativeComponent=_interopRequireDefault(require("react-native/Libraries/Utilities/codegenNativeComponent"));var _codegenNativeCommands=_interopRequireDefault(require("react-native/Libraries/Utilities/codegenNativeCommands"));var NativeComponentRegistry=require('react-native/Libraries/NativeComponent/NativeComponentRegistry');var _require=require('react-native/Libraries/NativeComponent/ViewConfigIgnore'),ConditionallyIgnoredEventHandlers=_require.ConditionallyIgnoredEventHandlers;var _require2=require("react-native/Libraries/ReactNative/RendererProxy"),dispatchCommand=_require2.dispatchCommand;var nativeComponentName='RNCWebView';var __INTERNAL_VIEW_CONFIG=exports.__INTERNAL_VIEW_CONFIG={uiViewClassName:'RNCWebView',directEventTypes:{topContentSizeChange:{registrationName:'onContentSizeChange'},topRenderProcessGone:{registrationName:'onRenderProcessGone'},topContentProcessDidTerminate:{registrationName:'onContentProcessDidTerminate'},topCustomMenuSelection:{registrationName:'onCustomMenuSelection'},topFileDownload:{registrationName:'onFileDownload'},topLoadingError:{registrationName:'onLoadingError'},topLoadingFinish:{registrationName:'onLoadingFinish'},topLoadingProgress:{registrationName:'onLoadingProgress'},topLoadingStart:{registrationName:'onLoadingStart'},topHttpError:{registrationName:'onHttpError'},topMessage:{registrationName:'onMessage'},topOpenWindow:{registrationName:'onOpenWindow'},topScroll:{registrationName:'onScroll'},topShouldStartLoadWithRequest:{registrationName:'onShouldStartLoadWithRequest'}},validAttributes:Object.assign({allowFileAccess:true,allowsProtectedMedia:true,allowsFullscreenVideo:true,androidLayerType:true,cacheMode:true,domStorageEnabled:true,downloadingMessage:true,forceDarkOn:true,geolocationEnabled:true,lackPermissionToDownloadMessage:true,messagingModuleName:true,minimumFontSize:true,mixedContentMode:true,nestedScrollEnabled:true,overScrollMode:true,saveFormDataDisabled:true,scalesPageToFit:true,setBuiltInZoomControls:true,setDisplayZoomControls:true,setSupportMultipleWindows:true,textZoom:true,thirdPartyCookiesEnabled:true,hasOnScroll:true,allowingReadAccessToURL:true,allowsBackForwardNavigationGestures:true,allowsInlineMediaPlayback:true,allowsPictureInPictureMediaPlayback:true,allowsAirPlayForMediaPlayback:true,allowsLinkPreview:true,automaticallyAdjustContentInsets:true,autoManageStatusBarEnabled:true,bounces:true,contentInset:true,contentInsetAdjustmentBehavior:true,contentMode:true,dataDetectorTypes:true,decelerationRate:true,directionalLockEnabled:true,enableApplePay:true,hideKeyboardAccessoryView:true,keyboardDisplayRequiresUserAction:true,limitsNavigationsToAppBoundDomains:true,mediaCapturePermissionGrantType:true,pagingEnabled:true,pullToRefreshEnabled:true,refreshControlLightMode:true,scrollEnabled:true,sharedCookiesEnabled:true,textInteractionEnabled:true,useSharedProcessPool:true,menuItems:true,suppressMenuItems:true,hasOnFileDownload:true,fraudulentWebsiteWarningEnabled:true,allowFileAccessFromFileURLs:true,allowUniversalAccessFromFileURLs:true,applicationNameForUserAgent:true,basicAuthCredential:true,cacheEnabled:true,incognito:true,injectedJavaScript:true,injectedJavaScriptBeforeContentLoaded:true,injectedJavaScriptForMainFrameOnly:true,injectedJavaScriptBeforeContentLoadedForMainFrameOnly:true,javaScriptCanOpenWindowsAutomatically:true,javaScriptEnabled:true,webviewDebuggingEnabled:true,mediaPlaybackRequiresUserAction:true,messagingEnabled:true,hasOnOpenWindowEvent:true,showsHorizontalScrollIndicator:true,showsVerticalScrollIndicator:true,indicatorStyle:true,newSource:true,userAgent:true,injectedJavaScriptObject:true,paymentRequestEnabled:true},ConditionallyIgnoredEventHandlers({onContentSizeChange:true,onRenderProcessGone:true,onContentProcessDidTerminate:true,onCustomMenuSelection:true,onFileDownload:true,onLoadingError:true,onLoadingFinish:true,onLoadingProgress:true,onLoadingStart:true,onHttpError:true,onMessage:true,onOpenWindow:true,onScroll:true,onShouldStartLoadWithRequest:true}))};var _default=exports.default=NativeComponentRegistry.get(nativeComponentName,function(){return __INTERNAL_VIEW_CONFIG;});var Commands=exports.Commands={goBack:function goBack(ref){dispatchCommand(ref,"goBack",[]);},goForward:function goForward(ref){dispatchCommand(ref,"goForward",[]);},reload:function reload(ref){dispatchCommand(ref,"reload",[]);},stopLoading:function stopLoading(ref){dispatchCommand(ref,"stopLoading",[]);},injectJavaScript:function injectJavaScript(ref,javascript){dispatchCommand(ref,"injectJavaScript",[javascript]);},requestFocus:function requestFocus(ref){dispatchCommand(ref,"requestFocus",[]);},postMessage:function postMessage(ref,data){dispatchCommand(ref,"postMessage",[data]);},loadUrl:function loadUrl(ref,url){dispatchCommand(ref,"loadUrl",[url]);},clearFormData:function clearFormData(ref){dispatchCommand(ref,"clearFormData",[]);},clearCache:function clearCache(ref,includeDiskFiles){dispatchCommand(ref,"clearCache",[includeDiskFiles]);},clearHistory:function clearHistory(ref){dispatchCommand(ref,"clearHistory",[]);}}; \ No newline at end of file +var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.default=exports.__INTERNAL_VIEW_CONFIG=exports.Commands=void 0;var _codegenNativeComponent=_interopRequireDefault(require("react-native/Libraries/Utilities/codegenNativeComponent"));var _codegenNativeCommands=_interopRequireDefault(require("react-native/Libraries/Utilities/codegenNativeCommands"));var NativeComponentRegistry=require('react-native/Libraries/NativeComponent/NativeComponentRegistry');var _require=require('react-native/Libraries/NativeComponent/ViewConfigIgnore'),ConditionallyIgnoredEventHandlers=_require.ConditionallyIgnoredEventHandlers;var _require2=require("react-native/Libraries/ReactNative/RendererProxy"),dispatchCommand=_require2.dispatchCommand;var nativeComponentName='RNCWebView';var __INTERNAL_VIEW_CONFIG=exports.__INTERNAL_VIEW_CONFIG={uiViewClassName:'RNCWebView',directEventTypes:{topContentSizeChange:{registrationName:'onContentSizeChange'},topRenderProcessGone:{registrationName:'onRenderProcessGone'},topContentProcessDidTerminate:{registrationName:'onContentProcessDidTerminate'},topCustomMenuSelection:{registrationName:'onCustomMenuSelection'},topFileDownload:{registrationName:'onFileDownload'},topLoadingError:{registrationName:'onLoadingError'},topLoadingSubResourceError:{registrationName:'onLoadingSubResourceError'},topLoadingFinish:{registrationName:'onLoadingFinish'},topLoadingProgress:{registrationName:'onLoadingProgress'},topLoadingStart:{registrationName:'onLoadingStart'},topHttpError:{registrationName:'onHttpError'},topMessage:{registrationName:'onMessage'},topOpenWindow:{registrationName:'onOpenWindow'},topScroll:{registrationName:'onScroll'},topShouldStartLoadWithRequest:{registrationName:'onShouldStartLoadWithRequest'}},validAttributes:Object.assign({allowFileAccess:true,allowsProtectedMedia:true,allowsFullscreenVideo:true,androidLayerType:true,cacheMode:true,domStorageEnabled:true,downloadingMessage:true,forceDarkOn:true,geolocationEnabled:true,lackPermissionToDownloadMessage:true,messagingModuleName:true,minimumFontSize:true,mixedContentMode:true,nestedScrollEnabled:true,overScrollMode:true,saveFormDataDisabled:true,scalesPageToFit:true,setBuiltInZoomControls:true,setDisplayZoomControls:true,setSupportMultipleWindows:true,textZoom:true,thirdPartyCookiesEnabled:true,hasOnScroll:true,allowingReadAccessToURL:true,allowsBackForwardNavigationGestures:true,allowsInlineMediaPlayback:true,allowsPictureInPictureMediaPlayback:true,allowsAirPlayForMediaPlayback:true,allowsLinkPreview:true,automaticallyAdjustContentInsets:true,autoManageStatusBarEnabled:true,bounces:true,contentInset:true,contentInsetAdjustmentBehavior:true,contentMode:true,dataDetectorTypes:true,decelerationRate:true,directionalLockEnabled:true,enableApplePay:true,hideKeyboardAccessoryView:true,keyboardDisplayRequiresUserAction:true,limitsNavigationsToAppBoundDomains:true,mediaCapturePermissionGrantType:true,pagingEnabled:true,pullToRefreshEnabled:true,refreshControlLightMode:true,scrollEnabled:true,sharedCookiesEnabled:true,textInteractionEnabled:true,useSharedProcessPool:true,menuItems:true,suppressMenuItems:true,hasOnFileDownload:true,fraudulentWebsiteWarningEnabled:true,allowFileAccessFromFileURLs:true,allowUniversalAccessFromFileURLs:true,applicationNameForUserAgent:true,basicAuthCredential:true,cacheEnabled:true,incognito:true,injectedJavaScript:true,injectedJavaScriptBeforeContentLoaded:true,injectedJavaScriptForMainFrameOnly:true,injectedJavaScriptBeforeContentLoadedForMainFrameOnly:true,javaScriptCanOpenWindowsAutomatically:true,javaScriptEnabled:true,webviewDebuggingEnabled:true,mediaPlaybackRequiresUserAction:true,messagingEnabled:true,hasOnOpenWindowEvent:true,showsHorizontalScrollIndicator:true,showsVerticalScrollIndicator:true,indicatorStyle:true,newSource:true,userAgent:true,injectedJavaScriptObject:true,paymentRequestEnabled:true},ConditionallyIgnoredEventHandlers({onContentSizeChange:true,onRenderProcessGone:true,onContentProcessDidTerminate:true,onCustomMenuSelection:true,onFileDownload:true,onLoadingError:true,onLoadingSubResourceError:true,onLoadingFinish:true,onLoadingProgress:true,onLoadingStart:true,onHttpError:true,onMessage:true,onOpenWindow:true,onScroll:true,onShouldStartLoadWithRequest:true}))};var _default=exports.default=NativeComponentRegistry.get(nativeComponentName,function(){return __INTERNAL_VIEW_CONFIG;});var Commands=exports.Commands={goBack:function goBack(ref){dispatchCommand(ref,"goBack",[]);},goForward:function goForward(ref){dispatchCommand(ref,"goForward",[]);},reload:function reload(ref){dispatchCommand(ref,"reload",[]);},stopLoading:function stopLoading(ref){dispatchCommand(ref,"stopLoading",[]);},injectJavaScript:function injectJavaScript(ref,javascript){dispatchCommand(ref,"injectJavaScript",[javascript]);},requestFocus:function requestFocus(ref){dispatchCommand(ref,"requestFocus",[]);},postMessage:function postMessage(ref,data){dispatchCommand(ref,"postMessage",[data]);},loadUrl:function loadUrl(ref,url){dispatchCommand(ref,"loadUrl",[url]);},clearFormData:function clearFormData(ref){dispatchCommand(ref,"clearFormData",[]);},clearCache:function clearCache(ref,includeDiskFiles){dispatchCommand(ref,"clearCache",[includeDiskFiles]);},clearHistory:function clearHistory(ref){dispatchCommand(ref,"clearHistory",[]);}}; \ No newline at end of file diff --git a/apps/expo-go/modules/react-native-webview/lib/WebView.android.js b/apps/expo-go/modules/react-native-webview/lib/WebView.android.js index 78e69c33f14538..c6cb7a95c15e96 100644 --- a/apps/expo-go/modules/react-native-webview/lib/WebView.android.js +++ b/apps/expo-go/modules/react-native-webview/lib/WebView.android.js @@ -1 +1 @@ -var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _defineProperty2=_interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));var _slicedToArray2=_interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));var _objectWithoutProperties2=_interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));var _react=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _BatchedBridge=_interopRequireDefault(require("react-native/Libraries/BatchedBridge/BatchedBridge"));var _EventEmitter=_interopRequireDefault(require("react-native/Libraries/vendor/emitter/EventEmitter"));var _invariant=_interopRequireDefault(require("invariant"));var _RNCWebViewNativeComponent=_interopRequireWildcard(require("./RNCWebViewNativeComponent"));var _NativeRNCWebViewModule=_interopRequireDefault(require("./NativeRNCWebViewModule"));var _WebViewShared=require("./WebViewShared");var _WebView=_interopRequireDefault(require("./WebView.styles"));var _jsxRuntime=require("react/jsx-runtime");var _excluded=["overScrollMode","javaScriptEnabled","thirdPartyCookiesEnabled","scalesPageToFit","allowsFullscreenVideo","allowFileAccess","saveFormDataDisabled","cacheEnabled","androidLayerType","originWhitelist","setSupportMultipleWindows","setBuiltInZoomControls","setDisplayZoomControls","nestedScrollEnabled","startInLoadingState","onNavigationStateChange","onLoadStart","onError","onLoad","onLoadEnd","onLoadProgress","onHttpError","onRenderProcessGone","onMessage","onOpenWindow","renderLoading","renderError","style","containerStyle","source","nativeConfig","onShouldStartLoadWithRequest","injectedJavaScriptObject"],_excluded2=["messagingModuleName"],_excluded3=["messagingModuleName"];var _require$registerCall,_this=this,_jsxFileName="/home/circleci/code/src/WebView.android.tsx";function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var r=new WeakMap(),t=new WeakMap();return(_getRequireWildcardCache=function _getRequireWildcardCache(e){return e?t:r;})(e);}function _interopRequireWildcard(e,r){if(!r&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=_getRequireWildcardCache(r);if(t&&t.has(e))return t.get(e);var n={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var i=a?Object.getOwnPropertyDescriptor(e,u):null;i&&(i.get||i.set)?Object.defineProperty(n,u,i):n[u]=e[u];}return n.default=e,t&&t.set(e,n),n;}var resolveAssetSource=_reactNative.Image.resolveAssetSource;var directEventEmitter=new _EventEmitter.default();var registerCallableModule=(_require$registerCall=require('react-native').registerCallableModule)!=null?_require$registerCall:_BatchedBridge.default.registerCallableModule.bind(_BatchedBridge.default);registerCallableModule('RNCWebViewMessagingModule',{onShouldStartLoadWithRequest:function onShouldStartLoadWithRequest(event){directEventEmitter.emit('onShouldStartLoadWithRequest',event);},onMessage:function onMessage(event){directEventEmitter.emit('onMessage',event);}});var uniqueRef=0;var WebViewComponent=(0,_react.forwardRef)(function(_ref,ref){var _ref$overScrollMode=_ref.overScrollMode,overScrollMode=_ref$overScrollMode===void 0?'always':_ref$overScrollMode,_ref$javaScriptEnable=_ref.javaScriptEnabled,javaScriptEnabled=_ref$javaScriptEnable===void 0?true:_ref$javaScriptEnable,_ref$thirdPartyCookie=_ref.thirdPartyCookiesEnabled,thirdPartyCookiesEnabled=_ref$thirdPartyCookie===void 0?true:_ref$thirdPartyCookie,_ref$scalesPageToFit=_ref.scalesPageToFit,scalesPageToFit=_ref$scalesPageToFit===void 0?true:_ref$scalesPageToFit,_ref$allowsFullscreen=_ref.allowsFullscreenVideo,allowsFullscreenVideo=_ref$allowsFullscreen===void 0?false:_ref$allowsFullscreen,_ref$allowFileAccess=_ref.allowFileAccess,allowFileAccess=_ref$allowFileAccess===void 0?false:_ref$allowFileAccess,_ref$saveFormDataDisa=_ref.saveFormDataDisabled,saveFormDataDisabled=_ref$saveFormDataDisa===void 0?false:_ref$saveFormDataDisa,_ref$cacheEnabled=_ref.cacheEnabled,cacheEnabled=_ref$cacheEnabled===void 0?true:_ref$cacheEnabled,_ref$androidLayerType=_ref.androidLayerType,androidLayerType=_ref$androidLayerType===void 0?'none':_ref$androidLayerType,_ref$originWhitelist=_ref.originWhitelist,originWhitelist=_ref$originWhitelist===void 0?_WebViewShared.defaultOriginWhitelist:_ref$originWhitelist,_ref$setSupportMultip=_ref.setSupportMultipleWindows,setSupportMultipleWindows=_ref$setSupportMultip===void 0?true:_ref$setSupportMultip,_ref$setBuiltInZoomCo=_ref.setBuiltInZoomControls,setBuiltInZoomControls=_ref$setBuiltInZoomCo===void 0?true:_ref$setBuiltInZoomCo,_ref$setDisplayZoomCo=_ref.setDisplayZoomControls,setDisplayZoomControls=_ref$setDisplayZoomCo===void 0?false:_ref$setDisplayZoomCo,_ref$nestedScrollEnab=_ref.nestedScrollEnabled,nestedScrollEnabled=_ref$nestedScrollEnab===void 0?false:_ref$nestedScrollEnab,startInLoadingState=_ref.startInLoadingState,onNavigationStateChange=_ref.onNavigationStateChange,onLoadStart=_ref.onLoadStart,onError=_ref.onError,onLoad=_ref.onLoad,onLoadEnd=_ref.onLoadEnd,onLoadProgress=_ref.onLoadProgress,onHttpErrorProp=_ref.onHttpError,onRenderProcessGoneProp=_ref.onRenderProcessGone,onMessageProp=_ref.onMessage,onOpenWindowProp=_ref.onOpenWindow,renderLoading=_ref.renderLoading,renderError=_ref.renderError,style=_ref.style,containerStyle=_ref.containerStyle,source=_ref.source,nativeConfig=_ref.nativeConfig,onShouldStartLoadWithRequestProp=_ref.onShouldStartLoadWithRequest,injectedJavaScriptObject=_ref.injectedJavaScriptObject,otherProps=(0,_objectWithoutProperties2.default)(_ref,_excluded);var messagingModuleName=(0,_react.useRef)(`WebViewMessageHandler${uniqueRef+=1}`).current;var webViewRef=(0,_react.useRef)(null);var onShouldStartLoadWithRequestCallback=(0,_react.useCallback)(function(shouldStart,url,lockIdentifier){if(lockIdentifier){_NativeRNCWebViewModule.default.shouldStartLoadWithLockIdentifier(shouldStart,lockIdentifier);}else if(shouldStart&&webViewRef.current){_RNCWebViewNativeComponent.Commands.loadUrl(webViewRef.current,url);}},[]);var _useWebViewLogic=(0,_WebViewShared.useWebViewLogic)({onNavigationStateChange:onNavigationStateChange,onLoad:onLoad,onError:onError,onHttpErrorProp:onHttpErrorProp,onLoadEnd:onLoadEnd,onLoadProgress:onLoadProgress,onLoadStart:onLoadStart,onRenderProcessGoneProp:onRenderProcessGoneProp,onMessageProp:onMessageProp,onOpenWindowProp:onOpenWindowProp,startInLoadingState:startInLoadingState,originWhitelist:originWhitelist,onShouldStartLoadWithRequestProp:onShouldStartLoadWithRequestProp,onShouldStartLoadWithRequestCallback:onShouldStartLoadWithRequestCallback}),onLoadingStart=_useWebViewLogic.onLoadingStart,onShouldStartLoadWithRequest=_useWebViewLogic.onShouldStartLoadWithRequest,onMessage=_useWebViewLogic.onMessage,viewState=_useWebViewLogic.viewState,setViewState=_useWebViewLogic.setViewState,lastErrorEvent=_useWebViewLogic.lastErrorEvent,onHttpError=_useWebViewLogic.onHttpError,onLoadingError=_useWebViewLogic.onLoadingError,onLoadingFinish=_useWebViewLogic.onLoadingFinish,onLoadingProgress=_useWebViewLogic.onLoadingProgress,onOpenWindow=_useWebViewLogic.onOpenWindow,onRenderProcessGone=_useWebViewLogic.onRenderProcessGone;(0,_react.useImperativeHandle)(ref,function(){return{goForward:function goForward(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.goForward(webViewRef.current);},goBack:function goBack(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.goBack(webViewRef.current);},reload:function reload(){setViewState('LOADING');if(webViewRef.current){_RNCWebViewNativeComponent.Commands.reload(webViewRef.current);}},stopLoading:function stopLoading(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.stopLoading(webViewRef.current);},postMessage:function postMessage(data){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.postMessage(webViewRef.current,data);},injectJavaScript:function injectJavaScript(data){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.injectJavaScript(webViewRef.current,data);},requestFocus:function requestFocus(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.requestFocus(webViewRef.current);},clearFormData:function clearFormData(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.clearFormData(webViewRef.current);},clearCache:function clearCache(includeDiskFiles){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.clearCache(webViewRef.current,includeDiskFiles);},clearHistory:function clearHistory(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.clearHistory(webViewRef.current);}};},[setViewState,webViewRef]);(0,_react.useEffect)(function(){var onShouldStartLoadWithRequestSubscription=directEventEmitter.addListener('onShouldStartLoadWithRequest',function(event){if(event.messagingModuleName===messagingModuleName){var _=event.messagingModuleName,rest=(0,_objectWithoutProperties2.default)(event,_excluded2);onShouldStartLoadWithRequest(rest);}});var onMessageSubscription=directEventEmitter.addListener('onMessage',function(event){if(event.messagingModuleName===messagingModuleName){var _=event.messagingModuleName,rest=(0,_objectWithoutProperties2.default)(event,_excluded3);onMessage(rest);}});return function(){onShouldStartLoadWithRequestSubscription.remove();onMessageSubscription.remove();};},[messagingModuleName,onMessage,onShouldStartLoadWithRequest]);var otherView;if(viewState==='LOADING'){otherView=(renderLoading||_WebViewShared.defaultRenderLoading)();}else if(viewState==='ERROR'){(0,_invariant.default)(lastErrorEvent!=null,'lastErrorEvent expected to be non-null');if(lastErrorEvent){otherView=(renderError||_WebViewShared.defaultRenderError)(lastErrorEvent.domain,lastErrorEvent.code,lastErrorEvent.description);}}else if(viewState!=='IDLE'){console.error(`RNCWebView invalid state encountered: ${viewState}`);}var webViewStyles=[_WebView.default.container,_WebView.default.webView,style];var webViewContainerStyle=[_WebView.default.container,containerStyle];if(typeof source!=='number'&&source&&'method'in source){if(source.method==='POST'&&source.headers){console.warn('WebView: `source.headers` is not supported when using POST.');}else if(source.method==='GET'&&source.body){console.warn('WebView: `source.body` is not supported when using GET.');}}var NativeWebView=(nativeConfig==null?void 0:nativeConfig.component)||_RNCWebViewNativeComponent.default;var sourceResolved=resolveAssetSource(source);var newSource=typeof sourceResolved==='object'?Object.entries(sourceResolved).reduce(function(prev,_ref2){var _ref3=(0,_slicedToArray2.default)(_ref2,2),currKey=_ref3[0],currValue=_ref3[1];return Object.assign({},prev,(0,_defineProperty2.default)({},currKey,currKey==='headers'&&currValue&&typeof currValue==='object'?Object.entries(currValue).map(function(_ref4){var _ref5=(0,_slicedToArray2.default)(_ref4,2),key=_ref5[0],value=_ref5[1];return{name:key,value:value};}):currValue));},{}):sourceResolved;var webView=(0,_jsxRuntime.jsx)(NativeWebView,Object.assign({},otherProps,{messagingEnabled:typeof onMessageProp==='function',messagingModuleName:messagingModuleName,hasOnScroll:!!otherProps.onScroll,onLoadingError:onLoadingError,onLoadingFinish:onLoadingFinish,onLoadingProgress:onLoadingProgress,onLoadingStart:onLoadingStart,onHttpError:onHttpError,onRenderProcessGone:onRenderProcessGone,onMessage:onMessage,onOpenWindow:onOpenWindow,hasOnOpenWindowEvent:onOpenWindowProp!==undefined,onShouldStartLoadWithRequest:onShouldStartLoadWithRequest,ref:webViewRef,source:sourceResolved,newSource:newSource,style:webViewStyles,overScrollMode:overScrollMode,javaScriptEnabled:javaScriptEnabled,thirdPartyCookiesEnabled:thirdPartyCookiesEnabled,scalesPageToFit:scalesPageToFit,allowsFullscreenVideo:allowsFullscreenVideo,allowFileAccess:allowFileAccess,saveFormDataDisabled:saveFormDataDisabled,cacheEnabled:cacheEnabled,androidLayerType:androidLayerType,setSupportMultipleWindows:setSupportMultipleWindows,setBuiltInZoomControls:setBuiltInZoomControls,setDisplayZoomControls:setDisplayZoomControls,nestedScrollEnabled:nestedScrollEnabled,injectedJavaScriptObject:JSON.stringify(injectedJavaScriptObject)},nativeConfig==null?void 0:nativeConfig.props),"webViewKey");return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:webViewContainerStyle,children:[webView,otherView]});});var isFileUploadSupported=_NativeRNCWebViewModule.default.isFileUploadSupported;var WebView=Object.assign(WebViewComponent,{isFileUploadSupported:isFileUploadSupported});var _default=exports.default=WebView; \ No newline at end of file +var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _defineProperty2=_interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));var _slicedToArray2=_interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));var _objectWithoutProperties2=_interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));var _react=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _BatchedBridge=_interopRequireDefault(require("react-native/Libraries/BatchedBridge/BatchedBridge"));var _EventEmitter=_interopRequireDefault(require("react-native/Libraries/vendor/emitter/EventEmitter"));var _invariant=_interopRequireDefault(require("invariant"));var _RNCWebViewNativeComponent=_interopRequireWildcard(require("./RNCWebViewNativeComponent"));var _NativeRNCWebViewModule=_interopRequireDefault(require("./NativeRNCWebViewModule"));var _WebViewShared=require("./WebViewShared");var _WebView=_interopRequireDefault(require("./WebView.styles"));var _jsxRuntime=require("react/jsx-runtime");var _excluded=["overScrollMode","javaScriptEnabled","thirdPartyCookiesEnabled","scalesPageToFit","allowsFullscreenVideo","allowFileAccess","saveFormDataDisabled","cacheEnabled","androidLayerType","originWhitelist","setSupportMultipleWindows","setBuiltInZoomControls","setDisplayZoomControls","nestedScrollEnabled","startInLoadingState","onNavigationStateChange","onLoadStart","onError","onLoad","onLoadEnd","onLoadSubResourceError","onLoadProgress","onHttpError","onRenderProcessGone","onMessage","onOpenWindow","renderLoading","renderError","style","containerStyle","source","nativeConfig","onShouldStartLoadWithRequest","injectedJavaScriptObject"],_excluded2=["messagingModuleName"],_excluded3=["messagingModuleName"];var _require$registerCall,_this=this,_jsxFileName="/home/circleci/code/src/WebView.android.tsx";function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var r=new WeakMap(),t=new WeakMap();return(_getRequireWildcardCache=function _getRequireWildcardCache(e){return e?t:r;})(e);}function _interopRequireWildcard(e,r){if(!r&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=_getRequireWildcardCache(r);if(t&&t.has(e))return t.get(e);var n={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var i=a?Object.getOwnPropertyDescriptor(e,u):null;i&&(i.get||i.set)?Object.defineProperty(n,u,i):n[u]=e[u];}return n.default=e,t&&t.set(e,n),n;}var resolveAssetSource=_reactNative.Image.resolveAssetSource;var directEventEmitter=new _EventEmitter.default();var registerCallableModule=(_require$registerCall=require('react-native').registerCallableModule)!=null?_require$registerCall:_BatchedBridge.default.registerCallableModule.bind(_BatchedBridge.default);registerCallableModule('RNCWebViewMessagingModule',{onShouldStartLoadWithRequest:function onShouldStartLoadWithRequest(event){directEventEmitter.emit('onShouldStartLoadWithRequest',event);},onMessage:function onMessage(event){directEventEmitter.emit('onMessage',event);}});var uniqueRef=0;var WebViewComponent=(0,_react.forwardRef)(function(_ref,ref){var _ref$overScrollMode=_ref.overScrollMode,overScrollMode=_ref$overScrollMode===void 0?'always':_ref$overScrollMode,_ref$javaScriptEnable=_ref.javaScriptEnabled,javaScriptEnabled=_ref$javaScriptEnable===void 0?true:_ref$javaScriptEnable,_ref$thirdPartyCookie=_ref.thirdPartyCookiesEnabled,thirdPartyCookiesEnabled=_ref$thirdPartyCookie===void 0?true:_ref$thirdPartyCookie,_ref$scalesPageToFit=_ref.scalesPageToFit,scalesPageToFit=_ref$scalesPageToFit===void 0?true:_ref$scalesPageToFit,_ref$allowsFullscreen=_ref.allowsFullscreenVideo,allowsFullscreenVideo=_ref$allowsFullscreen===void 0?false:_ref$allowsFullscreen,_ref$allowFileAccess=_ref.allowFileAccess,allowFileAccess=_ref$allowFileAccess===void 0?false:_ref$allowFileAccess,_ref$saveFormDataDisa=_ref.saveFormDataDisabled,saveFormDataDisabled=_ref$saveFormDataDisa===void 0?false:_ref$saveFormDataDisa,_ref$cacheEnabled=_ref.cacheEnabled,cacheEnabled=_ref$cacheEnabled===void 0?true:_ref$cacheEnabled,_ref$androidLayerType=_ref.androidLayerType,androidLayerType=_ref$androidLayerType===void 0?'none':_ref$androidLayerType,_ref$originWhitelist=_ref.originWhitelist,originWhitelist=_ref$originWhitelist===void 0?_WebViewShared.defaultOriginWhitelist:_ref$originWhitelist,_ref$setSupportMultip=_ref.setSupportMultipleWindows,setSupportMultipleWindows=_ref$setSupportMultip===void 0?true:_ref$setSupportMultip,_ref$setBuiltInZoomCo=_ref.setBuiltInZoomControls,setBuiltInZoomControls=_ref$setBuiltInZoomCo===void 0?true:_ref$setBuiltInZoomCo,_ref$setDisplayZoomCo=_ref.setDisplayZoomControls,setDisplayZoomControls=_ref$setDisplayZoomCo===void 0?false:_ref$setDisplayZoomCo,_ref$nestedScrollEnab=_ref.nestedScrollEnabled,nestedScrollEnabled=_ref$nestedScrollEnab===void 0?false:_ref$nestedScrollEnab,startInLoadingState=_ref.startInLoadingState,onNavigationStateChange=_ref.onNavigationStateChange,onLoadStart=_ref.onLoadStart,onError=_ref.onError,onLoad=_ref.onLoad,onLoadEnd=_ref.onLoadEnd,onLoadSubResourceError=_ref.onLoadSubResourceError,onLoadProgress=_ref.onLoadProgress,onHttpErrorProp=_ref.onHttpError,onRenderProcessGoneProp=_ref.onRenderProcessGone,onMessageProp=_ref.onMessage,onOpenWindowProp=_ref.onOpenWindow,renderLoading=_ref.renderLoading,renderError=_ref.renderError,style=_ref.style,containerStyle=_ref.containerStyle,source=_ref.source,nativeConfig=_ref.nativeConfig,onShouldStartLoadWithRequestProp=_ref.onShouldStartLoadWithRequest,injectedJavaScriptObject=_ref.injectedJavaScriptObject,otherProps=(0,_objectWithoutProperties2.default)(_ref,_excluded);var messagingModuleName=(0,_react.useRef)(`WebViewMessageHandler${uniqueRef+=1}`).current;var webViewRef=(0,_react.useRef)(null);var onShouldStartLoadWithRequestCallback=(0,_react.useCallback)(function(shouldStart,url,lockIdentifier){if(lockIdentifier){_NativeRNCWebViewModule.default.shouldStartLoadWithLockIdentifier(shouldStart,lockIdentifier);}else if(shouldStart&&webViewRef.current){_RNCWebViewNativeComponent.Commands.loadUrl(webViewRef.current,url);}},[]);var _useWebViewLogic=(0,_WebViewShared.useWebViewLogic)({onNavigationStateChange:onNavigationStateChange,onLoad:onLoad,onError:onError,onHttpErrorProp:onHttpErrorProp,onLoadSubResourceError:onLoadSubResourceError,onLoadEnd:onLoadEnd,onLoadProgress:onLoadProgress,onLoadStart:onLoadStart,onRenderProcessGoneProp:onRenderProcessGoneProp,onMessageProp:onMessageProp,onOpenWindowProp:onOpenWindowProp,startInLoadingState:startInLoadingState,originWhitelist:originWhitelist,onShouldStartLoadWithRequestProp:onShouldStartLoadWithRequestProp,onShouldStartLoadWithRequestCallback:onShouldStartLoadWithRequestCallback}),onLoadingStart=_useWebViewLogic.onLoadingStart,onShouldStartLoadWithRequest=_useWebViewLogic.onShouldStartLoadWithRequest,onMessage=_useWebViewLogic.onMessage,viewState=_useWebViewLogic.viewState,setViewState=_useWebViewLogic.setViewState,lastErrorEvent=_useWebViewLogic.lastErrorEvent,onHttpError=_useWebViewLogic.onHttpError,onLoadingError=_useWebViewLogic.onLoadingError,onLoadingSubResourceError=_useWebViewLogic.onLoadingSubResourceError,onLoadingFinish=_useWebViewLogic.onLoadingFinish,onLoadingProgress=_useWebViewLogic.onLoadingProgress,onOpenWindow=_useWebViewLogic.onOpenWindow,onRenderProcessGone=_useWebViewLogic.onRenderProcessGone;(0,_react.useImperativeHandle)(ref,function(){return{goForward:function goForward(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.goForward(webViewRef.current);},goBack:function goBack(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.goBack(webViewRef.current);},reload:function reload(){setViewState('LOADING');if(webViewRef.current){_RNCWebViewNativeComponent.Commands.reload(webViewRef.current);}},stopLoading:function stopLoading(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.stopLoading(webViewRef.current);},postMessage:function postMessage(data){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.postMessage(webViewRef.current,data);},injectJavaScript:function injectJavaScript(data){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.injectJavaScript(webViewRef.current,data);},requestFocus:function requestFocus(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.requestFocus(webViewRef.current);},clearFormData:function clearFormData(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.clearFormData(webViewRef.current);},clearCache:function clearCache(includeDiskFiles){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.clearCache(webViewRef.current,includeDiskFiles);},clearHistory:function clearHistory(){return webViewRef.current&&_RNCWebViewNativeComponent.Commands.clearHistory(webViewRef.current);}};},[setViewState,webViewRef]);(0,_react.useEffect)(function(){var onShouldStartLoadWithRequestSubscription=directEventEmitter.addListener('onShouldStartLoadWithRequest',function(event){if(event.messagingModuleName===messagingModuleName){var _=event.messagingModuleName,rest=(0,_objectWithoutProperties2.default)(event,_excluded2);onShouldStartLoadWithRequest(rest);}});var onMessageSubscription=directEventEmitter.addListener('onMessage',function(event){if(event.messagingModuleName===messagingModuleName){var _=event.messagingModuleName,rest=(0,_objectWithoutProperties2.default)(event,_excluded3);onMessage(rest);}});return function(){onShouldStartLoadWithRequestSubscription.remove();onMessageSubscription.remove();};},[messagingModuleName,onMessage,onShouldStartLoadWithRequest]);var otherView;if(viewState==='LOADING'){otherView=(renderLoading||_WebViewShared.defaultRenderLoading)();}else if(viewState==='ERROR'){(0,_invariant.default)(lastErrorEvent!=null,'lastErrorEvent expected to be non-null');if(lastErrorEvent){otherView=(renderError||_WebViewShared.defaultRenderError)(lastErrorEvent.domain,lastErrorEvent.code,lastErrorEvent.description);}}else if(viewState!=='IDLE'){console.error(`RNCWebView invalid state encountered: ${viewState}`);}var webViewStyles=[_WebView.default.container,_WebView.default.webView,style];var webViewContainerStyle=[_WebView.default.container,containerStyle];if(typeof source!=='number'&&source&&'method'in source){if(source.method==='POST'&&source.headers){console.warn('WebView: `source.headers` is not supported when using POST.');}else if(source.method==='GET'&&source.body){console.warn('WebView: `source.body` is not supported when using GET.');}}var NativeWebView=(nativeConfig==null?void 0:nativeConfig.component)||_RNCWebViewNativeComponent.default;var sourceResolved=resolveAssetSource(source);var newSource=typeof sourceResolved==='object'?Object.entries(sourceResolved).reduce(function(prev,_ref2){var _ref3=(0,_slicedToArray2.default)(_ref2,2),currKey=_ref3[0],currValue=_ref3[1];return Object.assign({},prev,(0,_defineProperty2.default)({},currKey,currKey==='headers'&&currValue&&typeof currValue==='object'?Object.entries(currValue).map(function(_ref4){var _ref5=(0,_slicedToArray2.default)(_ref4,2),key=_ref5[0],value=_ref5[1];return{name:key,value:value};}):currValue));},{}):sourceResolved;var webView=(0,_jsxRuntime.jsx)(NativeWebView,Object.assign({},otherProps,{messagingEnabled:typeof onMessageProp==='function',messagingModuleName:messagingModuleName,hasOnScroll:!!otherProps.onScroll,onLoadingError:onLoadingError,onLoadingSubResourceError:onLoadingSubResourceError,onLoadingFinish:onLoadingFinish,onLoadingProgress:onLoadingProgress,onLoadingStart:onLoadingStart,onHttpError:onHttpError,onRenderProcessGone:onRenderProcessGone,onMessage:onMessage,onOpenWindow:onOpenWindow,hasOnOpenWindowEvent:onOpenWindowProp!==undefined,onShouldStartLoadWithRequest:onShouldStartLoadWithRequest,ref:webViewRef,source:sourceResolved,newSource:newSource,style:webViewStyles,overScrollMode:overScrollMode,javaScriptEnabled:javaScriptEnabled,thirdPartyCookiesEnabled:thirdPartyCookiesEnabled,scalesPageToFit:scalesPageToFit,allowsFullscreenVideo:allowsFullscreenVideo,allowFileAccess:allowFileAccess,saveFormDataDisabled:saveFormDataDisabled,cacheEnabled:cacheEnabled,androidLayerType:androidLayerType,setSupportMultipleWindows:setSupportMultipleWindows,setBuiltInZoomControls:setBuiltInZoomControls,setDisplayZoomControls:setDisplayZoomControls,nestedScrollEnabled:nestedScrollEnabled,injectedJavaScriptObject:JSON.stringify(injectedJavaScriptObject)},nativeConfig==null?void 0:nativeConfig.props),"webViewKey");return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:webViewContainerStyle,children:[webView,otherView]});});var isFileUploadSupported=_NativeRNCWebViewModule.default.isFileUploadSupported;var WebView=Object.assign(WebViewComponent,{isFileUploadSupported:isFileUploadSupported});var _default=exports.default=WebView; \ No newline at end of file diff --git a/apps/expo-go/modules/react-native-webview/lib/WebViewShared.d.ts b/apps/expo-go/modules/react-native-webview/lib/WebViewShared.d.ts index 37344a794b0cff..8738f30fa18bec 100644 --- a/apps/expo-go/modules/react-native-webview/lib/WebViewShared.d.ts +++ b/apps/expo-go/modules/react-native-webview/lib/WebViewShared.d.ts @@ -5,7 +5,7 @@ declare const createOnShouldStartLoadWithRequest: (loadRequest: (shouldStart: bo declare const defaultRenderLoading: () => React.JSX.Element; declare const defaultRenderError: (errorDomain: string | undefined, errorCode: number, errorDesc: string) => React.JSX.Element; export { defaultOriginWhitelist, createOnShouldStartLoadWithRequest, defaultRenderLoading, defaultRenderError, }; -export declare const useWebViewLogic: ({ startInLoadingState, onNavigationStateChange, onLoadStart, onLoad, onLoadProgress, onLoadEnd, onError, onHttpErrorProp, onMessageProp, onOpenWindowProp, onRenderProcessGoneProp, onContentProcessDidTerminateProp, originWhitelist, onShouldStartLoadWithRequestProp, onShouldStartLoadWithRequestCallback, }: { +export declare const useWebViewLogic: ({ startInLoadingState, onNavigationStateChange, onLoadStart, onLoad, onLoadProgress, onLoadEnd, onError, onLoadSubResourceError, onHttpErrorProp, onMessageProp, onOpenWindowProp, onRenderProcessGoneProp, onContentProcessDidTerminateProp, originWhitelist, onShouldStartLoadWithRequestProp, onShouldStartLoadWithRequestCallback, }: { startInLoadingState?: boolean | undefined; onNavigationStateChange?: ((event: WebViewNavigation) => void) | undefined; onLoadStart?: ((event: WebViewNavigationEvent) => void) | undefined; @@ -13,6 +13,7 @@ export declare const useWebViewLogic: ({ startInLoadingState, onNavigationStateC onLoadProgress?: ((event: WebViewProgressEvent) => void) | undefined; onLoadEnd?: ((event: WebViewNavigationEvent | WebViewErrorEvent) => void) | undefined; onError?: ((event: WebViewErrorEvent) => void) | undefined; + onLoadSubResourceError?: ((event: WebViewErrorEvent) => void) | undefined; onHttpErrorProp?: ((event: WebViewHttpErrorEvent) => void) | undefined; onMessageProp?: ((event: WebViewMessageEvent) => void) | undefined; onOpenWindowProp?: ((event: WebViewOpenWindowEvent) => void) | undefined; @@ -26,6 +27,7 @@ export declare const useWebViewLogic: ({ startInLoadingState, onNavigationStateC onLoadingStart: (event: WebViewNavigationEvent) => void; onLoadingProgress: (event: WebViewProgressEvent) => void; onLoadingError: (event: WebViewErrorEvent) => void; + onLoadingSubResourceError: (event: WebViewErrorEvent) => void; onLoadingFinish: (event: WebViewNavigationEvent) => void; onHttpError: (event: WebViewHttpErrorEvent) => void; onRenderProcessGone: (event: WebViewRenderProcessGoneEvent) => void; diff --git a/apps/expo-go/modules/react-native-webview/lib/WebViewShared.js b/apps/expo-go/modules/react-native-webview/lib/WebViewShared.js index da517df30a206a..ff1da3083a969b 100644 --- a/apps/expo-go/modules/react-native-webview/lib/WebViewShared.js +++ b/apps/expo-go/modules/react-native-webview/lib/WebViewShared.js @@ -1 +1 @@ -var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.useWebViewLogic=exports.defaultRenderLoading=exports.defaultRenderError=exports.defaultOriginWhitelist=exports.createOnShouldStartLoadWithRequest=void 0;var _slicedToArray2=_interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));var _toConsumableArray2=_interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));var _escapeStringRegexp=_interopRequireDefault(require("escape-string-regexp"));var _react=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _WebView=_interopRequireDefault(require("./WebView.styles"));var _jsxRuntime=require("react/jsx-runtime");var _this=this,_jsxFileName="/home/circleci/code/src/WebViewShared.tsx";function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var r=new WeakMap(),t=new WeakMap();return(_getRequireWildcardCache=function _getRequireWildcardCache(e){return e?t:r;})(e);}function _interopRequireWildcard(e,r){if(!r&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=_getRequireWildcardCache(r);if(t&&t.has(e))return t.get(e);var n={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var i=a?Object.getOwnPropertyDescriptor(e,u):null;i&&(i.get||i.set)?Object.defineProperty(n,u,i):n[u]=e[u];}return n.default=e,t&&t.set(e,n),n;}var defaultOriginWhitelist=exports.defaultOriginWhitelist=['http://*','https://*'];var extractOrigin=function extractOrigin(url){var result=/^[A-Za-z][A-Za-z0-9+\-.]+:(\/\/)?[^/]*/.exec(url);return result===null?'':result[0];};var originWhitelistToRegex=function originWhitelistToRegex(originWhitelist){return`^${(0,_escapeStringRegexp.default)(originWhitelist).replace(/\\\*/g,'.*')}`;};var passesWhitelist=function passesWhitelist(compiledWhitelist,url){var origin=extractOrigin(url);return compiledWhitelist.some(function(x){return new RegExp(x).test(origin);});};var compileWhitelist=function compileWhitelist(originWhitelist){return['about:blank'].concat((0,_toConsumableArray2.default)(originWhitelist||[])).map(originWhitelistToRegex);};var createOnShouldStartLoadWithRequest=exports.createOnShouldStartLoadWithRequest=function createOnShouldStartLoadWithRequest(loadRequest,originWhitelist,onShouldStartLoadWithRequest){return function(_ref){var nativeEvent=_ref.nativeEvent;var shouldStart=true;var url=nativeEvent.url,lockIdentifier=nativeEvent.lockIdentifier;if(!passesWhitelist(compileWhitelist(originWhitelist),url)){_reactNative.Linking.canOpenURL(url).then(function(supported){if(supported){return _reactNative.Linking.openURL(url);}console.warn(`Can't open url: ${url}`);return undefined;}).catch(function(e){console.warn('Error opening URL: ',e);});shouldStart=false;}else if(onShouldStartLoadWithRequest){shouldStart=onShouldStartLoadWithRequest(nativeEvent);}loadRequest(shouldStart,url,lockIdentifier);};};var defaultRenderLoading=exports.defaultRenderLoading=function defaultRenderLoading(){return(0,_jsxRuntime.jsx)(_reactNative.View,{style:_WebView.default.loadingOrErrorView,children:(0,_jsxRuntime.jsx)(_reactNative.ActivityIndicator,{})});};var defaultRenderError=exports.defaultRenderError=function defaultRenderError(errorDomain,errorCode,errorDesc){return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:_WebView.default.loadingOrErrorView,children:[(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorTextTitle,children:"Error loading page"}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorText,children:`Domain: ${errorDomain}`}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorText,children:`Error Code: ${errorCode}`}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorText,children:`Description: ${errorDesc}`})]});};var useWebViewLogic=exports.useWebViewLogic=function useWebViewLogic(_ref2){var startInLoadingState=_ref2.startInLoadingState,onNavigationStateChange=_ref2.onNavigationStateChange,onLoadStart=_ref2.onLoadStart,onLoad=_ref2.onLoad,onLoadProgress=_ref2.onLoadProgress,onLoadEnd=_ref2.onLoadEnd,onError=_ref2.onError,onHttpErrorProp=_ref2.onHttpErrorProp,onMessageProp=_ref2.onMessageProp,onOpenWindowProp=_ref2.onOpenWindowProp,onRenderProcessGoneProp=_ref2.onRenderProcessGoneProp,onContentProcessDidTerminateProp=_ref2.onContentProcessDidTerminateProp,originWhitelist=_ref2.originWhitelist,onShouldStartLoadWithRequestProp=_ref2.onShouldStartLoadWithRequestProp,onShouldStartLoadWithRequestCallback=_ref2.onShouldStartLoadWithRequestCallback;var _useState=(0,_react.useState)(startInLoadingState?'LOADING':'IDLE'),_useState2=(0,_slicedToArray2.default)(_useState,2),viewState=_useState2[0],setViewState=_useState2[1];var _useState3=(0,_react.useState)(null),_useState4=(0,_slicedToArray2.default)(_useState3,2),lastErrorEvent=_useState4[0],setLastErrorEvent=_useState4[1];var startUrl=(0,_react.useRef)(null);var updateNavigationState=(0,_react.useCallback)(function(event){onNavigationStateChange==null?void 0:onNavigationStateChange(event.nativeEvent);},[onNavigationStateChange]);var onLoadingStart=(0,_react.useCallback)(function(event){startUrl.current=event.nativeEvent.url;onLoadStart==null?void 0:onLoadStart(event);updateNavigationState(event);},[onLoadStart,updateNavigationState]);var onLoadingError=(0,_react.useCallback)(function(event){event.persist();if(onError){onError(event);}else{console.warn('Encountered an error loading page',event.nativeEvent);}onLoadEnd==null?void 0:onLoadEnd(event);if(event.isDefaultPrevented()){return;}setViewState('ERROR');setLastErrorEvent(event.nativeEvent);},[onError,onLoadEnd]);var onHttpError=(0,_react.useCallback)(function(event){onHttpErrorProp==null?void 0:onHttpErrorProp(event);},[onHttpErrorProp]);var onRenderProcessGone=(0,_react.useCallback)(function(event){onRenderProcessGoneProp==null?void 0:onRenderProcessGoneProp(event);},[onRenderProcessGoneProp]);var onContentProcessDidTerminate=(0,_react.useCallback)(function(event){onContentProcessDidTerminateProp==null?void 0:onContentProcessDidTerminateProp(event);},[onContentProcessDidTerminateProp]);var onLoadingFinish=(0,_react.useCallback)(function(event){onLoad==null?void 0:onLoad(event);onLoadEnd==null?void 0:onLoadEnd(event);var url=event.nativeEvent.url;if(_reactNative.Platform.OS!=='android'||url===startUrl.current){setViewState('IDLE');}updateNavigationState(event);},[onLoad,onLoadEnd,updateNavigationState]);var onMessage=(0,_react.useCallback)(function(event){onMessageProp==null?void 0:onMessageProp(event);},[onMessageProp]);var onLoadingProgress=(0,_react.useCallback)(function(event){var progress=event.nativeEvent.progress;if(_reactNative.Platform.OS==='android'&&progress===1){setViewState(function(prevViewState){return prevViewState==='LOADING'?'IDLE':prevViewState;});}onLoadProgress==null?void 0:onLoadProgress(event);},[onLoadProgress]);var onShouldStartLoadWithRequest=(0,_react.useMemo)(function(){return createOnShouldStartLoadWithRequest(onShouldStartLoadWithRequestCallback,originWhitelist,onShouldStartLoadWithRequestProp);},[originWhitelist,onShouldStartLoadWithRequestProp,onShouldStartLoadWithRequestCallback]);var onOpenWindow=(0,_react.useCallback)(function(event){onOpenWindowProp==null?void 0:onOpenWindowProp(event);},[onOpenWindowProp]);return{onShouldStartLoadWithRequest:onShouldStartLoadWithRequest,onLoadingStart:onLoadingStart,onLoadingProgress:onLoadingProgress,onLoadingError:onLoadingError,onLoadingFinish:onLoadingFinish,onHttpError:onHttpError,onRenderProcessGone:onRenderProcessGone,onContentProcessDidTerminate:onContentProcessDidTerminate,onMessage:onMessage,onOpenWindow:onOpenWindow,viewState:viewState,setViewState:setViewState,lastErrorEvent:lastErrorEvent};}; \ No newline at end of file +var _interopRequireDefault=require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports,"__esModule",{value:true});exports.useWebViewLogic=exports.defaultRenderLoading=exports.defaultRenderError=exports.defaultOriginWhitelist=exports.createOnShouldStartLoadWithRequest=void 0;var _slicedToArray2=_interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));var _toConsumableArray2=_interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));var _escapeStringRegexp=_interopRequireDefault(require("escape-string-regexp"));var _react=_interopRequireWildcard(require("react"));var _reactNative=require("react-native");var _WebView=_interopRequireDefault(require("./WebView.styles"));var _jsxRuntime=require("react/jsx-runtime");var _this=this,_jsxFileName="/home/circleci/code/src/WebViewShared.tsx";function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var r=new WeakMap(),t=new WeakMap();return(_getRequireWildcardCache=function _getRequireWildcardCache(e){return e?t:r;})(e);}function _interopRequireWildcard(e,r){if(!r&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=_getRequireWildcardCache(r);if(t&&t.has(e))return t.get(e);var n={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var i=a?Object.getOwnPropertyDescriptor(e,u):null;i&&(i.get||i.set)?Object.defineProperty(n,u,i):n[u]=e[u];}return n.default=e,t&&t.set(e,n),n;}var defaultOriginWhitelist=exports.defaultOriginWhitelist=['http://*','https://*'];var extractOrigin=function extractOrigin(url){var result=/^[A-Za-z][A-Za-z0-9+\-.]+:(\/\/)?[^/]*/.exec(url);return result===null?'':result[0];};var originWhitelistToRegex=function originWhitelistToRegex(originWhitelist){return`^${(0,_escapeStringRegexp.default)(originWhitelist).replace(/\\\*/g,'.*')}`;};var passesWhitelist=function passesWhitelist(compiledWhitelist,url){var origin=extractOrigin(url);return compiledWhitelist.some(function(x){return new RegExp(x).test(origin);});};var compileWhitelist=function compileWhitelist(originWhitelist){return['about:blank'].concat((0,_toConsumableArray2.default)(originWhitelist||[])).map(originWhitelistToRegex);};var createOnShouldStartLoadWithRequest=exports.createOnShouldStartLoadWithRequest=function createOnShouldStartLoadWithRequest(loadRequest,originWhitelist,onShouldStartLoadWithRequest){return function(_ref){var nativeEvent=_ref.nativeEvent;var shouldStart=true;var url=nativeEvent.url,lockIdentifier=nativeEvent.lockIdentifier;if(!passesWhitelist(compileWhitelist(originWhitelist),url)){_reactNative.Linking.canOpenURL(url).then(function(supported){if(supported){return _reactNative.Linking.openURL(url);}console.warn(`Can't open url: ${url}`);return undefined;}).catch(function(e){console.warn('Error opening URL: ',e);});shouldStart=false;}else if(onShouldStartLoadWithRequest){shouldStart=onShouldStartLoadWithRequest(nativeEvent);}loadRequest(shouldStart,url,lockIdentifier);};};var defaultRenderLoading=exports.defaultRenderLoading=function defaultRenderLoading(){return(0,_jsxRuntime.jsx)(_reactNative.View,{style:_WebView.default.loadingOrErrorView,children:(0,_jsxRuntime.jsx)(_reactNative.ActivityIndicator,{})});};var defaultRenderError=exports.defaultRenderError=function defaultRenderError(errorDomain,errorCode,errorDesc){return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:_WebView.default.loadingOrErrorView,children:[(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorTextTitle,children:"Error loading page"}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorText,children:`Domain: ${errorDomain}`}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorText,children:`Error Code: ${errorCode}`}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:_WebView.default.errorText,children:`Description: ${errorDesc}`})]});};var useWebViewLogic=exports.useWebViewLogic=function useWebViewLogic(_ref2){var startInLoadingState=_ref2.startInLoadingState,onNavigationStateChange=_ref2.onNavigationStateChange,onLoadStart=_ref2.onLoadStart,onLoad=_ref2.onLoad,onLoadProgress=_ref2.onLoadProgress,onLoadEnd=_ref2.onLoadEnd,onError=_ref2.onError,onLoadSubResourceError=_ref2.onLoadSubResourceError,onHttpErrorProp=_ref2.onHttpErrorProp,onMessageProp=_ref2.onMessageProp,onOpenWindowProp=_ref2.onOpenWindowProp,onRenderProcessGoneProp=_ref2.onRenderProcessGoneProp,onContentProcessDidTerminateProp=_ref2.onContentProcessDidTerminateProp,originWhitelist=_ref2.originWhitelist,onShouldStartLoadWithRequestProp=_ref2.onShouldStartLoadWithRequestProp,onShouldStartLoadWithRequestCallback=_ref2.onShouldStartLoadWithRequestCallback;var _useState=(0,_react.useState)(startInLoadingState?'LOADING':'IDLE'),_useState2=(0,_slicedToArray2.default)(_useState,2),viewState=_useState2[0],setViewState=_useState2[1];var _useState3=(0,_react.useState)(null),_useState4=(0,_slicedToArray2.default)(_useState3,2),lastErrorEvent=_useState4[0],setLastErrorEvent=_useState4[1];var startUrl=(0,_react.useRef)(null);var updateNavigationState=(0,_react.useCallback)(function(event){onNavigationStateChange==null?void 0:onNavigationStateChange(event.nativeEvent);},[onNavigationStateChange]);var onLoadingStart=(0,_react.useCallback)(function(event){startUrl.current=event.nativeEvent.url;onLoadStart==null?void 0:onLoadStart(event);updateNavigationState(event);},[onLoadStart,updateNavigationState]);var onLoadingError=(0,_react.useCallback)(function(event){event.persist();if(onError){onError(event);}else{console.warn('Encountered an error loading page',event.nativeEvent);}onLoadEnd==null?void 0:onLoadEnd(event);if(event.isDefaultPrevented()){return;}setViewState('ERROR');setLastErrorEvent(event.nativeEvent);},[onError,onLoadEnd]);var onLoadingSubResourceError=(0,_react.useCallback)(function(event){onLoadSubResourceError==null?void 0:onLoadSubResourceError(event);},[onLoadSubResourceError]);var onHttpError=(0,_react.useCallback)(function(event){onHttpErrorProp==null?void 0:onHttpErrorProp(event);},[onHttpErrorProp]);var onRenderProcessGone=(0,_react.useCallback)(function(event){onRenderProcessGoneProp==null?void 0:onRenderProcessGoneProp(event);},[onRenderProcessGoneProp]);var onContentProcessDidTerminate=(0,_react.useCallback)(function(event){onContentProcessDidTerminateProp==null?void 0:onContentProcessDidTerminateProp(event);},[onContentProcessDidTerminateProp]);var onLoadingFinish=(0,_react.useCallback)(function(event){onLoad==null?void 0:onLoad(event);onLoadEnd==null?void 0:onLoadEnd(event);var url=event.nativeEvent.url;if(_reactNative.Platform.OS!=='android'||url===startUrl.current){setViewState('IDLE');}updateNavigationState(event);},[onLoad,onLoadEnd,updateNavigationState]);var onMessage=(0,_react.useCallback)(function(event){onMessageProp==null?void 0:onMessageProp(event);},[onMessageProp]);var onLoadingProgress=(0,_react.useCallback)(function(event){var progress=event.nativeEvent.progress;if(_reactNative.Platform.OS==='android'&&progress===1){setViewState(function(prevViewState){return prevViewState==='LOADING'?'IDLE':prevViewState;});}onLoadProgress==null?void 0:onLoadProgress(event);},[onLoadProgress]);var onShouldStartLoadWithRequest=(0,_react.useMemo)(function(){return createOnShouldStartLoadWithRequest(onShouldStartLoadWithRequestCallback,originWhitelist,onShouldStartLoadWithRequestProp);},[originWhitelist,onShouldStartLoadWithRequestProp,onShouldStartLoadWithRequestCallback]);var onOpenWindow=(0,_react.useCallback)(function(event){onOpenWindowProp==null?void 0:onOpenWindowProp(event);},[onOpenWindowProp]);return{onShouldStartLoadWithRequest:onShouldStartLoadWithRequest,onLoadingStart:onLoadingStart,onLoadingProgress:onLoadingProgress,onLoadingError:onLoadingError,onLoadingSubResourceError:onLoadingSubResourceError,onLoadingFinish:onLoadingFinish,onHttpError:onHttpError,onRenderProcessGone:onRenderProcessGone,onContentProcessDidTerminate:onContentProcessDidTerminate,onMessage:onMessage,onOpenWindow:onOpenWindow,viewState:viewState,setViewState:setViewState,lastErrorEvent:lastErrorEvent};}; \ No newline at end of file diff --git a/apps/expo-go/modules/react-native-webview/lib/WebViewTypes.d.ts b/apps/expo-go/modules/react-native-webview/lib/WebViewTypes.d.ts index 8cbc89feca8c77..998cc88f03f3fb 100644 --- a/apps/expo-go/modules/react-native-webview/lib/WebViewTypes.d.ts +++ b/apps/expo-go/modules/react-native-webview/lib/WebViewTypes.d.ts @@ -953,6 +953,13 @@ export interface AndroidWebViewProps extends WebViewSharedProps { * @platform android */ allowsProtectedMedia?: boolean; + /** + * Function that is invoked when the `WebView` receives an SSL error for a sub-resource. + * + * @param event + * @platform android + */ + onLoadSubResourceError?: (event: WebViewErrorEvent) => void; } export interface WebViewSharedProps extends ViewProps { /** diff --git a/apps/expo-go/modules/react-native-webview/package.json b/apps/expo-go/modules/react-native-webview/package.json index 9d2bd0acc12995..9162fc31acd358 100644 --- a/apps/expo-go/modules/react-native-webview/package.json +++ b/apps/expo-go/modules/react-native-webview/package.json @@ -10,7 +10,7 @@ "Thibault Malbranche " ], "license": "MIT", - "version": "13.15.0", + "version": "13.16.0", "homepage": "https://github.com/react-native-webview/react-native-webview#readme", "scripts": { "android": "react-native run-android", diff --git a/apps/expo-go/modules/react-native-webview/src/RNCWebViewNativeComponent.ts b/apps/expo-go/modules/react-native-webview/src/RNCWebViewNativeComponent.ts index 16b65ace4aaaba..7f4e6c4f73506e 100644 --- a/apps/expo-go/modules/react-native-webview/src/RNCWebViewNativeComponent.ts +++ b/apps/expo-go/modules/react-native-webview/src/RNCWebViewNativeComponent.ts @@ -270,6 +270,7 @@ export interface NativeProps extends ViewProps { mediaPlaybackRequiresUserAction?: WithDefault; messagingEnabled: boolean; onLoadingError: DirectEventHandler; + onLoadingSubResourceError: DirectEventHandler; onLoadingFinish: DirectEventHandler; onLoadingProgress: DirectEventHandler; onLoadingStart: DirectEventHandler; diff --git a/apps/expo-go/modules/react-native-webview/src/WebView.android.tsx b/apps/expo-go/modules/react-native-webview/src/WebView.android.tsx index edf06fafd1c52d..6497a8b1e547c6 100644 --- a/apps/expo-go/modules/react-native-webview/src/WebView.android.tsx +++ b/apps/expo-go/modules/react-native-webview/src/WebView.android.tsx @@ -83,6 +83,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>( onError, onLoad, onLoadEnd, + onLoadSubResourceError, onLoadProgress, onHttpError: onHttpErrorProp, onRenderProcessGone: onRenderProcessGoneProp, @@ -130,6 +131,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>( lastErrorEvent, onHttpError, onLoadingError, + onLoadingSubResourceError, onLoadingFinish, onLoadingProgress, onOpenWindow, @@ -139,6 +141,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>( onLoad, onError, onHttpErrorProp, + onLoadSubResourceError, onLoadEnd, onLoadProgress, onLoadStart, @@ -284,6 +287,7 @@ const WebViewComponent = forwardRef<{}, AndroidWebViewProps>( messagingModuleName={messagingModuleName} hasOnScroll={!!otherProps.onScroll} onLoadingError={onLoadingError} + onLoadingSubResourceError={onLoadingSubResourceError} onLoadingFinish={onLoadingFinish} onLoadingProgress={onLoadingProgress} onLoadingStart={onLoadingStart} diff --git a/apps/expo-go/modules/react-native-webview/src/WebViewShared.tsx b/apps/expo-go/modules/react-native-webview/src/WebViewShared.tsx index 08905ea61669ae..3b0a38b1cc99ce 100644 --- a/apps/expo-go/modules/react-native-webview/src/WebViewShared.tsx +++ b/apps/expo-go/modules/react-native-webview/src/WebViewShared.tsx @@ -104,6 +104,7 @@ export const useWebViewLogic = ({ onLoadProgress, onLoadEnd, onError, + onLoadSubResourceError, onHttpErrorProp, onMessageProp, onOpenWindowProp, @@ -120,6 +121,7 @@ export const useWebViewLogic = ({ onLoadProgress?: (event: WebViewProgressEvent) => void; onLoadEnd?: (event: WebViewNavigationEvent | WebViewErrorEvent) => void; onError?: (event: WebViewErrorEvent) => void; + onLoadSubResourceError?: (event: WebViewErrorEvent) => void; onHttpErrorProp?: (event: WebViewHttpErrorEvent) => void; onMessageProp?: (event: WebViewMessageEvent) => void; onOpenWindowProp?: (event: WebViewOpenWindowEvent) => void; @@ -178,6 +180,13 @@ export const useWebViewLogic = ({ [onError, onLoadEnd] ); + const onLoadingSubResourceError = useCallback( + (event: WebViewErrorEvent) => { + onLoadSubResourceError?.(event); + }, + [onLoadSubResourceError] + ); + const onHttpError = useCallback( (event: WebViewHttpErrorEvent) => { onHttpErrorProp?.(event); @@ -270,6 +279,7 @@ export const useWebViewLogic = ({ onLoadingStart, onLoadingProgress, onLoadingError, + onLoadingSubResourceError, onLoadingFinish, onHttpError, onRenderProcessGone, diff --git a/apps/expo-go/modules/react-native-webview/src/WebViewTypes.ts b/apps/expo-go/modules/react-native-webview/src/WebViewTypes.ts index c534d17f02c8f5..eca81415ab4867 100644 --- a/apps/expo-go/modules/react-native-webview/src/WebViewTypes.ts +++ b/apps/expo-go/modules/react-native-webview/src/WebViewTypes.ts @@ -1160,6 +1160,14 @@ export interface AndroidWebViewProps extends WebViewSharedProps { * @platform android */ allowsProtectedMedia?: boolean; + + /** + * Function that is invoked when the `WebView` receives an SSL error for a sub-resource. + * + * @param event + * @platform android + */ + onLoadSubResourceError?: (event: WebViewErrorEvent) => void; } export interface WebViewSharedProps extends ViewProps { diff --git a/apps/expo-go/package.json b/apps/expo-go/package.json index d4a6a4303b0b82..f17c703fd2f8a0 100644 --- a/apps/expo-go/package.json +++ b/apps/expo-go/package.json @@ -28,25 +28,24 @@ "@gorhom/bottom-sheet": "5.1.8", "@graphql-codegen/introspection": "^2.1.1", "@react-native-async-storage/async-storage": "2.2.0", - "@react-native-community/datetimepicker": "^8.4.4", + "@react-native-community/datetimepicker": "^8.6.0", "@react-native-community/netinfo": "11.4.1", - "@react-native-community/slider": "5.0.1", + "@react-native-community/slider": "5.1.1", "@react-native-masked-view/masked-view": "0.3.2", - "@react-native-picker/picker": "2.11.2", + "@react-native-picker/picker": "2.11.4", "@react-native-segmented-control/segmented-control": "2.5.7", "@react-navigation/bottom-tabs": "^7.7.3", "@react-navigation/elements": "^2.8.1", "@react-navigation/native": "^7.1.21", "@react-navigation/stack": "^7.6.7", - "@shopify/react-native-skia": "2.2.12", - "@stripe/stripe-react-native": "0.50.3", + "@shopify/react-native-skia": "2.4.14", + "@stripe/stripe-react-native": "0.57.2", "date-fns": "^2.28.0", "dedent": "^0.7.0", "es6-error": "^4.1.1", "expo": "~54.0.8", "expo-application": "~7.0.7", "expo-asset": "~12.0.8", - "expo-av": "~16.0.7", "expo-blur": "~15.0.7", "expo-camera": "~17.0.8", "expo-constants": "~18.0.9", @@ -69,19 +68,19 @@ "react-native": "0.83.1", "react-native-edge-to-edge": "1.6.0", "react-native-fade-in-image": "^1.6.1", - "react-native-gesture-handler": "~2.28.0", + "react-native-gesture-handler": "~2.30.0", "react-native-infinite-scroll-view": "^0.4.5", "react-native-keyboard-aware-scroll-view": "^0.9.5", - "react-native-keyboard-controller": "^1.18.5", - "react-native-maps": "1.20.1", - "react-native-pager-view": "6.9.1", + "react-native-keyboard-controller": "^1.20.4", + "react-native-maps": "1.26.20", + "react-native-pager-view": "8.0.0", "react-native-paper": "^5.12.5", - "react-native-reanimated": "4.2.0", - "react-native-safe-area-context": "5.6.0", + "react-native-reanimated": "4.2.1", + "react-native-safe-area-context": "5.6.2", "react-native-screens": "4.19.0", - "react-native-svg": "15.12.1", + "react-native-svg": "15.15.1", "react-native-view-shot": "4.0.3", - "react-native-webview": "13.15.0", + "react-native-webview": "13.16.0", "react-native-worklets": "0.7.1", "react-redux": "^7.2.0", "redux": "^4.0.5", diff --git a/apps/expo-go/src/FeatureFlags.ts b/apps/expo-go/src/FeatureFlags.ts deleted file mode 100644 index 5c0d21f93fd824..00000000000000 --- a/apps/expo-go/src/FeatureFlags.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { isDevice } from 'expo-device'; - -import Environment from './utils/Environment'; - -export default { - // Disable all project tools in the App Store client. - ENABLE_PROJECT_TOOLS: !Environment.IsIOSRestrictedBuild, - ENABLE_QR_CODE_BUTTON: isDevice && !Environment.IsIOSRestrictedBuild, - // Disable the clipboard button in the App Store client. - ENABLE_CLIPBOARD_BUTTON: !Environment.IsIOSRestrictedBuild, -}; diff --git a/apps/expo-go/src/HomeApp.tsx b/apps/expo-go/src/HomeApp.tsx deleted file mode 100644 index 27612089814ba7..00000000000000 --- a/apps/expo-go/src/HomeApp.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { darkTheme, lightTheme } from '@expo/styleguide-native'; -import Ionicons from '@expo/vector-icons/build/Ionicons'; -import MaterialIcons from '@expo/vector-icons/build/MaterialIcons'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { Assets as StackAssets } from '@react-navigation/elements'; -import { Asset } from 'expo-asset'; -import { ThemePreference, ThemeProvider } from 'expo-dev-client-components'; -import * as Font from 'expo-font'; -import * as SplashScreen from 'expo-splash-screen'; -import * as React from 'react'; -import { Linking, Platform, StyleSheet, View, useColorScheme } from 'react-native'; -import url from 'url'; - -import ApolloClient from './api/ApolloClient'; -import { ColorTheme } from './constants/Colors'; -import { - AppPlatform, - HomeScreenDataDocument, - HomeScreenDataQuery, - HomeScreenDataQueryVariables, - Home_CurrentUserActorDocument, - Home_CurrentUserActorQuery, - Home_CurrentUserActorQueryVariables, -} from './graphql/types'; -import Navigation from './navigation/Navigation'; -import HistoryActions from './redux/HistoryActions'; -import { useDispatch, useSelector } from './redux/Hooks'; -import SessionActions from './redux/SessionActions'; -import SettingsActions from './redux/SettingsActions'; -import LocalStorage from './storage/LocalStorage'; -import { useAccountName } from './utils/AccountNameContext'; -import { useInitialData } from './utils/InitialDataContext'; -import * as UrlUtils from './utils/UrlUtils'; -import addListenerWithNativeCallback from './utils/addListenerWithNativeCallback'; - -// Download and cache stack assets, don't block loading on this though -Asset.loadAsync(StackAssets); - -function useSplashScreenWhileLoadingResources(loadResources: () => Promise) { - const [isSplashScreenShown, setSplashScreenShown] = React.useState(true); - React.useEffect(() => { - (async () => { - // await SplashScreen.preventAutoHideAsync(); // this is called in App (main component of the application) - await loadResources(); - setSplashScreenShown(false); - })(); - }, []); - React.useEffect(() => { - (async () => { - if (!isSplashScreenShown) { - await SplashScreen.hideAsync(); - } - })(); - }, [isSplashScreenShown]); - - return isSplashScreenShown; -} - -export default function HomeApp() { - const colorScheme = useColorScheme(); - const preferredAppearance = useSelector((data) => data.settings.preferredAppearance); - const dispatch = useDispatch(); - const { setAccountName } = useAccountName(); - const isShowingSplashScreen = useSplashScreenWhileLoadingResources(async () => { - await initStateAsync(); - }); - - const { setCurrentUserData, setHomeScreenData } = useInitialData(); - - React.useEffect(() => { - addProjectHistoryListener(); - }, []); - - React.useEffect(() => { - if (!isShowingSplashScreen && Platform.OS === 'ios') { - // If Expo Go is opened via deep linking, we'll get the URL here - Linking.getInitialURL().then((initialUrl) => { - if (initialUrl && shouldOpenUrl(initialUrl)) { - Linking.openURL(UrlUtils.toExp(initialUrl)); - } - }); - } - }, [isShowingSplashScreen]); - - const addProjectHistoryListener = () => { - addListenerWithNativeCallback('ExponentKernel.addHistoryItem', async (event) => { - let { manifestUrl, manifest, manifestString } = event; - if (!manifest && manifestString) { - manifest = JSON.parse(manifestString); - } - dispatch(HistoryActions.addHistoryItem(manifestUrl, manifest)); - }); - }; - - const loadFontsAsync = async () => { - try { - await Promise.all([ - Font.loadAsync(Ionicons.font), - Platform.OS === 'android' - ? Font.loadAsync(MaterialIcons.font) - : new Promise((resolve) => setTimeout(resolve, 0)), - Font.loadAsync({ - 'Inter-Black': require('./assets/Inter/Inter-Black.otf'), - 'Inter-BlackItalic': require('./assets/Inter/Inter-BlackItalic.otf'), - 'Inter-Bold': require('./assets/Inter/Inter-Bold.otf'), - 'Inter-BoldItalic': require('./assets/Inter/Inter-BoldItalic.otf'), - 'Inter-ExtraBold': require('./assets/Inter/Inter-ExtraBold.otf'), - 'Inter-ExtraBoldItalic': require('./assets/Inter/Inter-ExtraBoldItalic.otf'), - 'Inter-ExtraLight': require('./assets/Inter/Inter-ExtraLight.otf'), - 'Inter-ExtraLightItalic': require('./assets/Inter/Inter-ExtraLightItalic.otf'), - 'Inter-Regular': require('./assets/Inter/Inter-Regular.otf'), - 'Inter-Italic': require('./assets/Inter/Inter-Italic.otf'), - 'Inter-Light': require('./assets/Inter/Inter-Light.otf'), - 'Inter-LightItalic': require('./assets/Inter/Inter-LightItalic.otf'), - 'Inter-Medium': require('./assets/Inter/Inter-Medium.otf'), - 'Inter-MediumItalic': require('./assets/Inter/Inter-MediumItalic.otf'), - 'Inter-SemiBold': require('./assets/Inter/Inter-SemiBold.otf'), - 'Inter-SemiBoldItalic': require('./assets/Inter/Inter-SemiBoldItalic.otf'), - 'Inter-Thin': require('./assets/Inter/Inter-Thin.otf'), - 'Inter-ThinItalic': require('./assets/Inter/Inter-ThinItalic.otf'), - }), - ]); - } finally { - return; - } - }; - - const initStateAsync = async () => { - try { - dispatch(SettingsActions.loadSettings()); - dispatch(HistoryActions.loadHistory()); - - const storedSession = await LocalStorage.getSessionAsync(); - - if (storedSession) { - dispatch(SessionActions.setSession(storedSession)); - } - - const [currentUserQueryResult, persistedCurrentAccount] = await Promise.all([ - ApolloClient.query({ - query: Home_CurrentUserActorDocument, - context: { headers: { 'expo-session': storedSession?.sessionSecret } }, - }), - AsyncStorage.getItem('currentAccount'), - loadFontsAsync(), - ]); - - if (currentUserQueryResult.data && currentUserQueryResult.data.meUserActor) { - let firstLoadAccountName = persistedCurrentAccount; - if (firstLoadAccountName) { - // if there was a persisted account, and it matches the accounts available to the current user, use it - if ( - [ - currentUserQueryResult.data.meUserActor.username, - ...currentUserQueryResult.data.meUserActor.accounts.map((account) => account.name), - ].includes(firstLoadAccountName) - ) { - setAccountName(firstLoadAccountName); - } else { - // if this persisted account is stale, clear it - await AsyncStorage.removeItem('currentAccount'); - } - } else { - // if there was no persisted account, use the current user's personal account - firstLoadAccountName = currentUserQueryResult.data.meUserActor.username; - setAccountName(firstLoadAccountName); - } - - // set initial data for home screen - - setCurrentUserData(currentUserQueryResult.data); - - if (firstLoadAccountName) { - const homeScreenData = await ApolloClient.query< - HomeScreenDataQuery, - HomeScreenDataQueryVariables - >({ - query: HomeScreenDataDocument, - variables: { - accountName: firstLoadAccountName, - platform: Platform.OS === 'ios' ? AppPlatform.Ios : AppPlatform.Android, - }, - context: { headers: { 'expo-session': storedSession?.sessionSecret } }, - }); - - setHomeScreenData(homeScreenData.data); - } - } else { - // if there is no current user data, clear the accountName - setAccountName(undefined); - } - } finally { - return; - } - }; - - if (isShowingSplashScreen) { - return null; - } - - let theme = !preferredAppearance ? colorScheme : preferredAppearance; - if (theme === undefined || theme === null || (theme !== 'dark' && theme !== 'light')) { - theme = 'light'; - } - - const backgroundColor = - theme === 'dark' ? darkTheme.background.default : lightTheme.background.default; - - return ( - - - - - - ); -} - -// Certain links (i.e. 'expo.dev/expo-go') should just open the HomeScreen -function shouldOpenUrl(urlString: string) { - const parsedUrl = url.parse(urlString); - return !( - (parsedUrl.hostname === 'expo.io' || parsedUrl.hostname === 'expo.dev') && - parsedUrl.pathname === '/expo-go' - ); -} - -const styles = StyleSheet.create({ - container: { flex: 1 }, -}); diff --git a/apps/expo-go/src/api/APIV2Client.ts b/apps/expo-go/src/api/APIV2Client.ts deleted file mode 100644 index 005863f869306d..00000000000000 --- a/apps/expo-go/src/api/APIV2Client.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Platform } from 'react-native'; - -import { ApiError } from './ApiError'; -import Config from './Config'; -import { GenericError } from './GenericError'; -import * as Kernel from '../kernel/Kernel'; -import Store from '../redux/Store'; - -type SendOptions = { - method?: string; - headers?: Record; - body?: object; - searchParams?: Record; -}; -export class APIV2Client { - private async sendApiV2Request(route: string, options: SendOptions): Promise { - const url = new URL(`${Config.api.origin}/--/api/v2/${route}`); - if (options.searchParams) { - url.search = new URLSearchParams(options?.searchParams).toString(); - } - - let response: Response; - try { - response = await fetch(url.toString(), { - method: options.method ?? 'POST', - body: options.body ? JSON.stringify(options.body) : null, - headers: { - ...options.headers, - accept: 'application/json', - ...(options.body ? { 'content-type': 'application/json' } : null), - }, - }); - } catch (error) { - throw new GenericError( - `Something went wrong when connecting to Expo: ${(error as Error).message}.` - ); - } - - let text: string; - try { - text = await response.text(); - } catch (error) { - throw new GenericError( - `Something went wrong when reading the response (HTTP ${response.status}) from Expo: ${ - (error as Error).message - }.` - ); - } - - let body: any; - try { - body = JSON.parse(text); - } catch { - throw new GenericError(`The Expo server responded in an unexpected way: ${text}`); - } - - if (Array.isArray(body.errors) && body.errors.length > 0) { - const responseError = body.errors[0]; - const errorMessage = responseError.details - ? responseError.details.message - : responseError.message; - const error = new ApiError(errorMessage, responseError.code); - error.serverStack = responseError.stack; - error.metadata = responseError.metadata; - throw error; - } - - if (!response.ok) { - throw new GenericError(`The Expo server responded with a ${response.status} error.`); - } - - return body.data; - } - - public async sendAuthenticatedApiV2Request( - route: string, - options: SendOptions = {} - ): Promise { - const { session } = Store.getState(); - - const sessionSecret = session.sessionSecret; - - if (!sessionSecret) { - throw new ApiError('Must be logged in to perform request'); - } - - const newOptions = { - ...options, - headers: { - ...options.headers, - ...(sessionSecret - ? { - 'Expo-SDK-Version': Kernel.sdkVersions, - 'Expo-Platform': Platform.OS, - 'Expo-Session': sessionSecret, - } - : {}), - }, - }; - return await this.sendApiV2Request(route, newOptions); - } - - public async sendOptionallyAuthenticatedApiV2Request( - route: string, - options: SendOptions = {} - ): Promise { - const { session } = Store.getState(); - - const sessionSecret = session.sessionSecret; - const newOptions = { - ...options, - headers: { - ...options.headers, - ...(sessionSecret - ? { - 'Expo-SDK-Version': Kernel.sdkVersions, - 'Expo-Platform': Platform.OS, - 'Expo-Session': sessionSecret, - } - : {}), - }, - }; - return await this.sendApiV2Request(route, newOptions); - } - - public async sendUnauthenticatedApiV2Request( - route: string, - options: SendOptions = {} - ): Promise { - return await this.sendApiV2Request(route, options); - } -} diff --git a/apps/expo-go/src/api/ApiError.ts b/apps/expo-go/src/api/ApiError.ts deleted file mode 100644 index 6c89c8e45b65b4..00000000000000 --- a/apps/expo-go/src/api/ApiError.ts +++ /dev/null @@ -1,12 +0,0 @@ -import ExtendableError from 'es6-error'; - -export class ApiError extends ExtendableError { - code: string; - serverStack?: string; - metadata?: object; - - constructor(message: string, code: string = 'UNKNOWN') { - super(message); - this.code = code; - } -} diff --git a/apps/expo-go/src/api/ApiV2Error.ts b/apps/expo-go/src/api/ApiV2Error.ts deleted file mode 100644 index e007c447848277..00000000000000 --- a/apps/expo-go/src/api/ApiV2Error.ts +++ /dev/null @@ -1,12 +0,0 @@ -import ExtendableError from 'es6-error'; - -export default class ApiV2Error extends ExtendableError { - code: string; - serverStack?: string; - metadata?: object; - - constructor(message: string, code: string = 'UNKNOWN') { - super(message); - this.code = code; - } -} diff --git a/apps/expo-go/src/api/ApolloClient.ts b/apps/expo-go/src/api/ApolloClient.ts deleted file mode 100644 index a5727da7b90f56..00000000000000 --- a/apps/expo-go/src/api/ApolloClient.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ApolloClient, InMemoryCache, from } from '@apollo/client'; -import { setContext } from '@apollo/client/link/context'; -import { HttpLink } from '@apollo/client/link/http'; -import { offsetLimitPagination } from '@apollo/client/utilities'; - -import Config from './Config'; -import Connectivity from './Connectivity'; -import Store from '../redux/Store'; - -const httpLink = new HttpLink({ - uri: `${Config.api.origin}/--/graphql`, -}); - -const connectivityLink = setContext(async (): Promise => { - const isConnected = await Connectivity.isAvailableAsync(); - if (!isConnected) { - throw new Error('No connection available'); - } -}); - -const authMiddlewareLink = setContext((_request, previousContext): any => { - const { sessionSecret } = Store.getState().session; - - if (sessionSecret) { - return { - ...previousContext, - headers: { - 'expo-session': sessionSecret, - ...previousContext.headers, - }, - }; - } - - return previousContext; -}); - -const link = from([connectivityLink, authMiddlewareLink, httpLink]); - -const cache = new InMemoryCache({ - possibleTypes: { - AccountUsageMetadata: ['AccountUsageEASBuildMetadata'], - ActivityTimelineProjectActivity: ['Build', 'BuildJob', 'Submission', 'Update'], - Actor: ['Robot', 'SSOUser', 'User'], - BuildOrBuildJob: ['Build', 'BuildJob'], - EASBuildOrClassicBuildJob: ['Build', 'BuildJob'], - FcmSnippet: ['FcmSnippetLegacy', 'FcmSnippetV1'], - PlanEnablement: ['Concurrencies', 'EASTotalPlanEnablement'], - Project: ['App', 'Snack'], - UserActor: ['SSOUser', 'User'], - }, - addTypename: true, - typePolicies: { - Query: { - fields: { - account: { - merge: false, - }, - app: { - merge: false, - }, - }, - }, - Account: { - fields: { - apps: offsetLimitPagination(), - snacks: offsetLimitPagination(), - }, - }, - App: { - fields: { - updateBranches: offsetLimitPagination(), - }, - }, - }, -}); - -export default new ApolloClient({ - link, - cache, -}); diff --git a/apps/expo-go/src/api/AuthApi.ts b/apps/expo-go/src/api/AuthApi.ts deleted file mode 100644 index 41482a96d6fe9d..00000000000000 --- a/apps/expo-go/src/api/AuthApi.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { APIV2Client } from './APIV2Client'; - -async function signOutAsync(sessionSecret: string | null): Promise { - if (!sessionSecret) { - return; - } - const api = new APIV2Client(); - await api.sendAuthenticatedApiV2Request('auth/logout'); -} - -export default { - signOutAsync, -}; diff --git a/apps/expo-go/src/api/Config.ts b/apps/expo-go/src/api/Config.ts deleted file mode 100644 index 54568c423285fb..00000000000000 --- a/apps/expo-go/src/api/Config.ts +++ /dev/null @@ -1,18 +0,0 @@ -const host = 'exp.host'; -// const host = 'staging.exp.host'; -// const host = 'localhost:3000'; -//const host = 'ed56a018.ngrok.io'; -const origin = `https://${host}`; -// const origin = 'http://localhost:3000'; -const websiteOrigin = 'https://expo.dev'; -// const websiteOrigin = 'http://localhost:3001'; - -export default { - api: { - host, - origin, - }, - website: { - origin: websiteOrigin, - }, -}; diff --git a/apps/expo-go/src/api/Connectivity.ts b/apps/expo-go/src/api/Connectivity.ts deleted file mode 100644 index 289cbb2376103b..00000000000000 --- a/apps/expo-go/src/api/Connectivity.ts +++ /dev/null @@ -1,50 +0,0 @@ -import NetInfo, { NetInfoState } from '@react-native-community/netinfo'; - -type ConnectivityListener = (available: boolean) => void; - -class Connectivity { - _isAvailable = true; - _listeners = new Set(); - - constructor() { - NetInfo.addEventListener(this._handleConnectivityChange); - this.isAvailableAsync(); - } - - isAvailable(): boolean { - return this._isAvailable; - } - - async isAvailableAsync(): Promise { - if (this._isAvailable) { - return this._isAvailable; - } - - try { - const netInfo = await NetInfo.fetch(); - this._isAvailable = netInfo.isConnected ?? false; - } catch (e) { - this._isAvailable = false; - console.warn(`Uncaught error when fetching connectivity status: ${e}`); - } - - return this._isAvailable; - } - - _handleConnectivityChange = (netInfo: NetInfoState) => { - this._isAvailable = netInfo.isConnected ?? false; - this._listeners.forEach((listener) => { - listener(this._isAvailable); - }); - }; - - addListener(listener: ConnectivityListener): void { - this._listeners.add(listener); - } - - removeListener(listener: ConnectivityListener): void { - this._listeners.delete(listener); - } -} - -export default new Connectivity(); diff --git a/apps/expo-go/src/api/GenericError.ts b/apps/expo-go/src/api/GenericError.ts deleted file mode 100644 index f6a8ddc4912698..00000000000000 --- a/apps/expo-go/src/api/GenericError.ts +++ /dev/null @@ -1,3 +0,0 @@ -import ExtendableError from 'es6-error'; - -export class GenericError extends ExtendableError {} diff --git a/apps/expo-go/src/api/__tests__/AuthSessions-test.js b/apps/expo-go/src/api/__tests__/AuthSessions-test.js deleted file mode 100644 index 0b6786409a010c..00000000000000 --- a/apps/expo-go/src/api/__tests__/AuthSessions-test.js +++ /dev/null @@ -1,107 +0,0 @@ -import { uuid } from 'expo-modules-core'; -import gql from 'graphql-tag'; - -jest.mock('@react-native-async-storage/async-storage', () => ({ - setItem: jest.fn(() => new Promise((resolve) => resolve(null))), - getItem: jest.fn(() => new Promise((resolve) => resolve(null))), - removeItem: jest.fn(() => new Promise((resolve) => resolve(null))), -})); - -jest.mock('@react-native-community/netinfo'); - -describe('User Authentication Flow', () => { - let Store; - let SessionActions; - - let originalFetch; - - beforeEach(async () => { - originalFetch = global.fetch; - global.fetch = jest.fn(); - - Store = require('../../redux/Store').default; - SessionActions = require('../../redux/SessionActions').default; - - await Store.dispatch(SessionActions.signOut()); - }); - - afterEach(() => { - global.fetch = originalFetch; - originalFetch = null; - - jest.restoreAllMocks(); - }); - - it(`logs in and stores session tokens correctly`, async () => { - const { sessionSecret } = { sessionSecret: uuid.v4() }; - - // store session token - await Store.dispatch(SessionActions.setSession({ sessionSecret })); - - // retrieve session token - const state = Store.getState(); - const retrievedSession = state.session; - - // make sure the retrieved token is the same as the one we originally received - expect(sessionSecret).toBeDefined(); - expect(sessionSecret).toBe(retrievedSession.sessionSecret); - }); - - it(`performs authenticated GraphQL queries`, async () => { - const ApolloClient = require('../ApolloClient').default; - const apolloLinkRequest = jest.spyOn(ApolloClient.link, 'request'); - - const { sessionSecret } = { sessionSecret: uuid.v4() }; - - // store session token - await Store.dispatch(SessionActions.setSession({ sessionSecret })); - - _setFakeHttpResponse( - JSON.stringify({ - data: { - app: { - __typename: 'AppQuery', - all: [ - { __typename: 'App', id: '2c28de10-a2cd-11e6-b8ce-59d1587e6774' }, - { __typename: 'App', id: '0d4823c0-37fb-11e7-9c45-89e7ab918dda' }, - ], - }, - }, - }) - ); - - try { - await ApolloClient.query({ - query: gql` - { - app { - all(limit: 2, filter: NEW, sort: TOP) { - id - } - } - } - `, - variables: null, - }); - } finally { - ApolloClient.resetStore(); - } - - // expect the query to be authenticated - expect(apolloLinkRequest).toHaveBeenCalledTimes(1); - const operation = apolloLinkRequest.mock.calls[0][0]; - expect(operation.getContext().headers).toMatchObject({ - 'expo-session': sessionSecret, - }); - }); -}); - -function _setFakeHttpResponse(responseText) { - global.fetch.mockReturnValue( - Promise.resolve({ - async text() { - return responseText; - }, - }) - ); -} diff --git a/apps/expo-go/src/assets/Inter/Inter-Black.otf b/apps/expo-go/src/assets/Inter/Inter-Black.otf deleted file mode 100644 index 8e18e362ba5bbb..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-Black.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-BlackItalic.otf b/apps/expo-go/src/assets/Inter/Inter-BlackItalic.otf deleted file mode 100644 index e2144c297191ef..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-BlackItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-Bold.otf b/apps/expo-go/src/assets/Inter/Inter-Bold.otf deleted file mode 100644 index c74cc0c6c13ccd..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-Bold.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-BoldItalic.otf b/apps/expo-go/src/assets/Inter/Inter-BoldItalic.otf deleted file mode 100644 index 20d20c195cd7d7..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-BoldItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-ExtraBold.otf b/apps/expo-go/src/assets/Inter/Inter-ExtraBold.otf deleted file mode 100644 index 3633fe8e859330..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-ExtraBold.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-ExtraBoldItalic.otf b/apps/expo-go/src/assets/Inter/Inter-ExtraBoldItalic.otf deleted file mode 100644 index 1b54cfa37867dd..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-ExtraBoldItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-ExtraLight.otf b/apps/expo-go/src/assets/Inter/Inter-ExtraLight.otf deleted file mode 100644 index 6dc068dc90b394..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-ExtraLight.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-ExtraLightItalic.otf b/apps/expo-go/src/assets/Inter/Inter-ExtraLightItalic.otf deleted file mode 100644 index b68e7316a5d8e7..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-ExtraLightItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-Italic.otf b/apps/expo-go/src/assets/Inter/Inter-Italic.otf deleted file mode 100644 index 39d6016be45b57..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-Italic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-Light.otf b/apps/expo-go/src/assets/Inter/Inter-Light.otf deleted file mode 100644 index 2a83ae168def2d..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-Light.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-LightItalic.otf b/apps/expo-go/src/assets/Inter/Inter-LightItalic.otf deleted file mode 100644 index ca9fb12767262a..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-LightItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-Medium.otf b/apps/expo-go/src/assets/Inter/Inter-Medium.otf deleted file mode 100644 index ca7bfcd4340ea6..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-Medium.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-MediumItalic.otf b/apps/expo-go/src/assets/Inter/Inter-MediumItalic.otf deleted file mode 100644 index 2df62bc189f9ed..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-MediumItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-Regular.otf b/apps/expo-go/src/assets/Inter/Inter-Regular.otf deleted file mode 100644 index 84e6a61c3c0f11..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-Regular.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-SemiBold.otf b/apps/expo-go/src/assets/Inter/Inter-SemiBold.otf deleted file mode 100644 index daf4c4413f7b68..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-SemiBold.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-SemiBoldItalic.otf b/apps/expo-go/src/assets/Inter/Inter-SemiBoldItalic.otf deleted file mode 100644 index bc58b80d70b24c..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-SemiBoldItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-Thin.otf b/apps/expo-go/src/assets/Inter/Inter-Thin.otf deleted file mode 100644 index 22592e045c7240..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-Thin.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/Inter/Inter-ThinItalic.otf b/apps/expo-go/src/assets/Inter/Inter-ThinItalic.otf deleted file mode 100644 index c5ea2ecd712336..00000000000000 Binary files a/apps/expo-go/src/assets/Inter/Inter-ThinItalic.otf and /dev/null differ diff --git a/apps/expo-go/src/assets/client-logo.png b/apps/expo-go/src/assets/client-logo.png deleted file mode 100644 index 0b2bcef7d85750..00000000000000 Binary files a/apps/expo-go/src/assets/client-logo.png and /dev/null differ diff --git a/apps/expo-go/src/assets/ios-menu-copy.png b/apps/expo-go/src/assets/ios-menu-copy.png deleted file mode 100644 index b9e567e05b511d..00000000000000 Binary files a/apps/expo-go/src/assets/ios-menu-copy.png and /dev/null differ diff --git a/apps/expo-go/src/assets/ios-menu-home.png b/apps/expo-go/src/assets/ios-menu-home.png deleted file mode 100644 index 474e7eea8e261f..00000000000000 Binary files a/apps/expo-go/src/assets/ios-menu-home.png and /dev/null differ diff --git a/apps/expo-go/src/assets/ios-menu-information-circle.png b/apps/expo-go/src/assets/ios-menu-information-circle.png deleted file mode 100644 index e4dec642c138e9..00000000000000 Binary files a/apps/expo-go/src/assets/ios-menu-information-circle.png and /dev/null differ diff --git a/apps/expo-go/src/assets/ios-menu-refresh.png b/apps/expo-go/src/assets/ios-menu-refresh.png deleted file mode 100644 index 707f1c6e129178..00000000000000 Binary files a/apps/expo-go/src/assets/ios-menu-refresh.png and /dev/null differ diff --git a/apps/expo-go/src/assets/menu-md-close.png b/apps/expo-go/src/assets/menu-md-close.png deleted file mode 100644 index 67449d8a851519..00000000000000 Binary files a/apps/expo-go/src/assets/menu-md-close.png and /dev/null differ diff --git a/apps/expo-go/src/assets/placeholder-app-icon.png b/apps/expo-go/src/assets/placeholder-app-icon.png deleted file mode 100644 index 6eaf3029532605..00000000000000 Binary files a/apps/expo-go/src/assets/placeholder-app-icon.png and /dev/null differ diff --git a/apps/expo-go/src/components/AudioPlayer.tsx b/apps/expo-go/src/components/AudioPlayer.tsx deleted file mode 100644 index 0e9eac90188a50..00000000000000 --- a/apps/expo-go/src/components/AudioPlayer.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import Ionicons from '@expo/vector-icons/build/Ionicons'; -import { Audio, AVPlaybackStatus } from 'expo-av'; -import React, { useEffect, useState } from 'react'; -import { StyleSheet, Text, View, StyleProp, ViewStyle } from 'react-native'; -import { BorderlessButton, ScrollView } from 'react-native-gesture-handler'; - -import { StyledText } from '../components/Text'; -import Colors from '../constants/Colors'; - -interface Props { - isAudioEnabled: boolean; - source: { uri: string }; - style?: StyleProp; -} - -type ErrorState = string | null | undefined; - -type PlaybackState = - | { isLoaded: false } - | { - isLoaded: true; - sound: Audio.Sound; - positionMillis: number; - durationMillis: number | undefined; - isPlaying: boolean; - isLooping: boolean; - isMuted: boolean; - rate: number; - shouldCorrectPitch: boolean; - }; - -const initialErrorState: ErrorState = null; -const initialPlaybackState: PlaybackState = { isLoaded: false }; - -export default function AudioPlayer({ isAudioEnabled, source, style }: Props) { - const [error, setError] = useState(initialErrorState); - const [playback, setPlayback] = useState(initialPlaybackState); - - function handlePlaybackStatusUpdate(sound: Audio.Sound, status: AVPlaybackStatus): void { - if (!status.isLoaded) { - setPlayback({ isLoaded: false }); - setError(status.error); - } else { - setPlayback({ - isLoaded: true, - sound, - positionMillis: status.positionMillis, - durationMillis: status.durationMillis, - isPlaying: status.isPlaying, - isLooping: status.isLooping, - isMuted: status.isMuted, - rate: status.rate, - shouldCorrectPitch: status.shouldCorrectPitch, - }); - setError(null); - } - } - - useEffect(() => { - const sound = new Audio.Sound(); - sound.setOnPlaybackStatusUpdate((status) => { - handlePlaybackStatusUpdate(sound, status); - }); - - sound.loadAsync(source, { progressUpdateIntervalMillis: 150 }).catch(setError); - - return () => { - setPlayback({ isLoaded: false }); - sound.setOnPlaybackStatusUpdate(null); - sound.unloadAsync(); - }; - }, [source]); - - const isPlayable = isAudioEnabled && playback.isLoaded; - - return ( - - - { - if (playback.isLoaded) { - if (playback.isPlaying) { - playback.sound.pauseAsync(); - } else if (playback.positionMillis < playback.durationMillis!) { - playback.sound.playFromPositionAsync(0); - } else { - playback.sound.playAsync(); - } - } - }} - /> - - {playback.isLoaded - ? `${_formatTime(playback.positionMillis / 1000)} / ${_formatTime( - playback.durationMillis! / 1000 - )}` - : '00:00 / 00:00'} - - - - { - if (playback.isLoaded) { - playback.sound.setIsLoopingAsync(!playback.isLooping); - } - }} - /> - { - if (playback.isLoaded) { - const newRate = playback.rate < 1 ? 1 : 0.5; - playback.sound.setRateAsync( - newRate, - playback.shouldCorrectPitch, - Audio.PitchCorrectionQuality.High - ); - } - }} - /> - 1} - onPress={() => { - if (playback.isLoaded) { - const newRate = playback.rate > 1 ? 1 : 2; - playback.sound.setRateAsync( - newRate, - playback.shouldCorrectPitch, - Audio.PitchCorrectionQuality.High - ); - } - }} - /> - { - if (playback.isLoaded) { - playback.sound.setRateAsync( - playback.rate, - !playback.shouldCorrectPitch, - Audio.PitchCorrectionQuality.High - ); - } - }} - /> - { - if (playback.isLoaded) { - playback.sound.setIsMutedAsync(!playback.isMuted); - } - }} - /> - - {error ? : null} - - ); -} - -type AudioSettingsButtonProps = { - title: string; - iconName: 'hourglass' | 'volume-off' | 'repeat' | 'speedometer' | 'stats-chart'; - disabled: boolean; - active: boolean; - onPress: () => void; - style?: any; -}; - -function AudioSettingsButton(props: AudioSettingsButtonProps) { - return ( - - - - {props.title} - - - ); -} - -type PlayerErrorOverlayProps = { - errorMessage: string; -}; - -function PlayerErrorOverlay(props: PlayerErrorOverlayProps) { - return ( - - {props.errorMessage} - - ); -} - -type AudioPlayButtonProps = { - disabled: boolean; - active: boolean; - onPress: () => void; - style?: any; -}; - -function AudioPlayButton(props: AudioPlayButtonProps) { - return ( - - - - ); -} - -function _formatTime(duration: number): string { - const paddedSeconds = `${Math.floor(duration % 60)}`.padStart(2, '0'); - const paddedMinutes = `${Math.floor(duration / 60)}`.padStart(2, '0'); - return `${paddedMinutes}:${paddedSeconds}`; -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - }, - icon: { - color: Colors.light.tintColor, - fontSize: 24, - padding: 8, - }, - disabledButton: { - color: Colors.light.greyText, - }, - playButtonIcon: { - fontSize: 34, - paddingTop: 11, - }, - slider: { - flex: 1, - marginHorizontal: 10, - }, - buttonContainer: { - alignItems: 'stretch', - flexDirection: 'row', - justifyContent: 'space-evenly', - }, - button: { - flex: 1, - marginHorizontal: 10, - paddingBottom: 6, - borderRadius: 6, - marginBottom: 10, - alignItems: 'center', - justifyContent: 'flex-start', - }, - buttonText: { - fontSize: 12, - color: Colors.light.tintColor, - fontWeight: 'bold', - textAlign: 'center', - justifyContent: 'center', - flex: 1, - }, - buttonIcon: { - flex: 1, - height: 36, - }, - activeButton: { - backgroundColor: Colors.light.tintColor, - borderRadius: 12, - }, - activeButtonText: { - color: 'white', - }, - errorOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: '#f00', - }, - errorText: { - margin: 8, - fontWeight: 'bold', - color: '#fff', - }, -}); diff --git a/apps/expo-go/src/components/BranchListItem.tsx b/apps/expo-go/src/components/BranchListItem.tsx deleted file mode 100644 index 246d3f11890d39..00000000000000 --- a/apps/expo-go/src/components/BranchListItem.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { BranchIcon, UpdateIcon, iconSize, ChevronDownIcon } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { StackNavigationProp } from '@react-navigation/stack'; -import format from 'date-fns/format'; -import { Row, Spacer, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { DateFormats } from '../constants/DateFormats'; -import { ProjectsQuery } from '../graphql/types'; -import { HomeStackRoutes } from '../navigation/Navigation.types'; - -type Update = ProjectsQuery['app']['byId']['updateBranches'][number]['updates'][number]; - -type Props = { - name: string; - latestUpdate?: Update; - appId: string; - first: boolean; - last: boolean; -}; - -/** - * This component is used to render a list item for the branches section on the project screen and on - * the branches list page for an app. - */ - -export function BranchListItem({ name, appId, latestUpdate, first, last }: Props) { - const theme = useExpoTheme(); - - const navigation = useNavigation>(); - - const handlePressBranch = () => { - navigation.navigate('BranchDetails', { appId, branchName: name }); - }; - - return ( - - - - - - - - - - Branch: {name} - - - {latestUpdate?.message && ( - <> - - - - - - - - "{latestUpdate.message}" - - - - Published {format(new Date(latestUpdate.createdAt), DateFormats.timestamp)} - - - - - )} - - - - - - - - ); -} diff --git a/apps/expo-go/src/components/Button.tsx b/apps/expo-go/src/components/Button.tsx deleted file mode 100644 index 5f1b78c2d1cbe2..00000000000000 --- a/apps/expo-go/src/components/Button.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { borderRadius } from '@expo/styleguide-native'; -import { ExpoTheme, scale, Text, useExpoTheme } from 'expo-dev-client-components'; -import React from 'react'; -import { ActivityIndicator, TouchableOpacity, ViewStyle } from 'react-native'; - -type Theme = 'primary' | 'secondary' | 'tertiary' | 'ghost' | 'error'; - -type Props = { - label: string; - onPress: () => void; - theme?: Theme; - disabled?: boolean; - loading?: boolean; - style?: ViewStyle; -}; - -function getThemeColors( - theme: Theme, - expoTheme: ExpoTheme -): { backgroundColor: string; borderColor?: string; borderWidth?: 1; color: string } { - switch (theme) { - case 'primary': - return { - backgroundColor: expoTheme.button.primary.background, - color: expoTheme.button.primary.foreground, - }; - case 'secondary': - return { - backgroundColor: expoTheme.button.secondary.background, - color: expoTheme.button.secondary.foreground, - }; - case 'tertiary': - return { - backgroundColor: expoTheme.button.tertiary.background, - color: expoTheme.button.tertiary.foreground, - }; - case 'ghost': - return { - backgroundColor: expoTheme.button.ghost.background, - color: expoTheme.button.ghost.foreground, - borderColor: expoTheme.button.ghost.border, - borderWidth: 1, - }; - case 'error': - return { - backgroundColor: expoTheme.background.error, - color: expoTheme.text.error, - borderColor: expoTheme.border.error, - borderWidth: 1, - }; - } -} - -export function Button({ label, theme = 'tertiary', onPress, loading, disabled, style }: Props) { - const expoTheme = useExpoTheme(); - - const { backgroundColor, borderColor, borderWidth, color } = getThemeColors(theme, expoTheme); - - return ( - - {loading ? ( - - ) : ( - - {label} - - )} - - ); -} diff --git a/apps/expo-go/src/components/Camera.tsx b/apps/expo-go/src/components/Camera.tsx deleted file mode 100644 index e4e2853e2b378c..00000000000000 --- a/apps/expo-go/src/components/Camera.tsx +++ /dev/null @@ -1 +0,0 @@ -export { CameraView as default } from 'expo-camera'; diff --git a/apps/expo-go/src/components/ConstantItem.tsx b/apps/expo-go/src/components/ConstantItem.tsx deleted file mode 100644 index 0b037597e5db0e..00000000000000 --- a/apps/expo-go/src/components/ConstantItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Text, Row } from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -type Props = { - title: string; - value: string; - onPress?: () => void; -}; - -export function ConstantItem({ title, value, onPress }: Props) { - return ( - - - {title} - {value} - - - ); -} diff --git a/apps/expo-go/src/components/DevIndicator.tsx b/apps/expo-go/src/components/DevIndicator.tsx deleted file mode 100644 index 85fc690cb02557..00000000000000 --- a/apps/expo-go/src/components/DevIndicator.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import { View, ViewProps } from 'react-native'; - -export default function DevIndicator({ - style, - isActive, - isNetworkAvailable, -}: { - style: ViewProps['style']; - isActive?: boolean; - isNetworkAvailable?: boolean; -}) { - const backgroundColor = React.useMemo(() => { - if (isActive && isNetworkAvailable) { - return '#00c100'; - } else if (!isNetworkAvailable) { - return '#e0e057'; - } - return '#ccc'; - }, [isNetworkAvailable, isActive]); - - return ( - - ); -} diff --git a/apps/expo-go/src/components/Icons.tsx b/apps/expo-go/src/components/Icons.tsx deleted file mode 100644 index 0c658e50ad50ee..00000000000000 --- a/apps/expo-go/src/components/Icons.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import DefaultIonicons from '@expo/vector-icons/build/Ionicons'; -import DefaultMaterialIcons from '@expo/vector-icons/build/MaterialIcons'; -import { useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { Svg, Path, SvgProps } from 'react-native-svg'; - -type Props = { - size?: number; - style?: any; - lightColor?: string; - darkColor?: string; - color?: string; -}; - -type IconiconNames = React.ComponentProps['name']; -export const Ionicons = (props: Props & { name: IconiconNames }) => { - const theme = useTheme(); - const darkColor = props.darkColor || '#fff'; - const lightColor = props.lightColor || '#ccc'; - - return ; -}; - -type MaterialIconNames = React.ComponentProps['name']; -export const MaterialIcons = (props: Props & { name: MaterialIconNames }) => { - const theme = useTheme(); - const darkColor = props.darkColor || '#fff'; - const lightColor = props.lightColor || '#ccc'; - - return ; -}; - -export function Share({ size, color, ...props }: { color: string; size: number }) { - const fill = color; - return ( - - - - - ); -} - -export default function DiagnosticsIcon(props: SvgProps & Props) { - const { size, color, width, height } = props; - return ( - - - - - - ); -} diff --git a/apps/expo-go/src/components/NavigationEvents.tsx b/apps/expo-go/src/components/NavigationEvents.tsx deleted file mode 100644 index 912712ad22a9d4..00000000000000 --- a/apps/expo-go/src/components/NavigationEvents.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useFocusEffect } from '@react-navigation/native'; -import * as React from 'react'; - -export default function NavigationEvents(props: { children?: any; onDidFocus: () => void }) { - useFocusEffect( - React.useCallback(() => { - props.onDidFocus(); - }, [props.onDidFocus]) - ); - - return props.children ?? null; -} diff --git a/apps/expo-go/src/components/NavigationScrollView.tsx b/apps/expo-go/src/components/NavigationScrollView.tsx deleted file mode 100644 index 88e16a3a177bd3..00000000000000 --- a/apps/expo-go/src/components/NavigationScrollView.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useScrollToTop } from '@react-navigation/native'; -import React, { PropsWithChildren, useRef } from 'react'; -import { ScrollViewProps, ScrollView as RNScrollView, Platform } from 'react-native'; -import { - NativeViewGestureHandlerProps, - ScrollView as RNGHScrollView, -} from 'react-native-gesture-handler'; - -type StyledScrollViewProps = PropsWithChildren< - ScrollViewProps & - NativeViewGestureHandlerProps & { - lightBackgroundColor?: string; - darkBackgroundColor?: string; - } ->; - -export default function NavigationScrollView({ style, ...otherProps }: StyledScrollViewProps) { - const ref = useRef(null); - - useScrollToTop(ref); - - const ScrollView = Platform.OS === 'android' ? RNScrollView : RNGHScrollView; - - return ; -} diff --git a/apps/expo-go/src/components/PlatformIcon.tsx b/apps/expo-go/src/components/PlatformIcon.tsx deleted file mode 100644 index b348b27022b84a..00000000000000 --- a/apps/expo-go/src/components/PlatformIcon.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import { Platform, View, StyleSheet } from 'react-native'; - -import { Ionicons } from './Icons'; - -type Props = { - platform: 'native' | 'web'; -}; - -const style = Platform.select({ - android: { marginTop: 3 }, - default: {}, -}); - -export default function PlatformIcon(props: Props) { - const { platform } = props; - let icon: React.ReactNode = null; - if (platform === 'native') { - icon = Platform.select({ - android: , - ios: , - default: , - }); - } else if (platform === 'web') { - icon = ; - } - - return {icon}; -} - -const styles = StyleSheet.create({ - container: { - width: 17, - }, -}); diff --git a/apps/expo-go/src/components/PrimaryButton.tsx b/apps/expo-go/src/components/PrimaryButton.tsx deleted file mode 100644 index 32f8ffdb1880f8..00000000000000 --- a/apps/expo-go/src/components/PrimaryButton.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from 'react'; -import { - ActivityIndicator, - Platform, - StyleSheet, - Text, - TouchableNativeFeedback, - TouchableOpacity, - View, -} from 'react-native'; - -import Colors from '../constants/Colors'; - -type TouchableNativeFeedbackProps = React.ComponentProps; -export default function PrimaryButton({ - children, - isLoading, - plain, - style, - ...props -}: TouchableNativeFeedbackProps & { - children: any; - isLoading?: boolean; - plain?: boolean; -}) { - return Platform.OS === 'android' ? ( - - - {children} - {isLoading && ( - - - - )} - - - ) : ( - - {children} - {isLoading && ( - - - - )} - - ); -} - -const styles = StyleSheet.create({ - activityIndicatorContainer: { - position: 'absolute', - top: 0, - right: 15, - bottom: 0, - justifyContent: 'center', - }, - button: { - backgroundColor: Colors.light.tintColor, - paddingHorizontal: 30, - paddingVertical: 15, - borderRadius: 4, - }, - buttonText: { - color: '#fff', - textAlign: 'center', - lineHeight: 20, - ...Platform.select({ - android: { - fontSize: 16, - }, - ios: { - fontSize: 15, - fontWeight: '600', - }, - }), - }, - plainButton: {}, - plainButtonText: { - color: Colors.light.tintColor, - textAlign: 'center', - ...Platform.select({ - android: { - fontSize: 16, - }, - ios: { - fontSize: 15, - }, - }), - }, -}); diff --git a/apps/expo-go/src/components/ProjectsListItem.tsx b/apps/expo-go/src/components/ProjectsListItem.tsx deleted file mode 100644 index 22e04e6864fecb..00000000000000 --- a/apps/expo-go/src/components/ProjectsListItem.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { ChevronDownIcon } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { Row, Spacer, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import React from 'react'; -import { Platform, StyleSheet } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import { CommonAppDataFragment } from 'src/graphql/types'; -import { - isUpdateCompatibleWithThisExpoGo, - openUpdateManifestPermalink, -} from 'src/utils/UpdateUtils'; - -import { HomeStackRoutes } from '../navigation/Navigation.types'; -import { AppIcon } from '../screens/HomeScreen/AppIcon'; - -type Props = { - name: string; - firstTwoBranches: CommonAppDataFragment['firstTwoBranches']; - subtitle?: string; - id: string; - first: boolean; - last: boolean; -}; - -/** - * This component is used to render a list item for the projects section on the homescreen and on - * the projects list page for an account. - */ - -export function ProjectsListItem({ name, subtitle, firstTwoBranches, id, first, last }: Props) { - const theme = useExpoTheme(); - - const navigation = useNavigation>(); - - function onPress() { - // if there's only one branch for the project and there's only one update on that branch and - // it is compatible with this version of Expo Go, just launch it upon tapping the row instead of entering - // a drill-down UI for exploring branches/updates. - if (firstTwoBranches.length === 1) { - const branch = firstTwoBranches[0]; - if (branch.updates.length === 1) { - const updateToOpen = branch.updates[0]; - const isCompatibleWithThisExpoGo = isUpdateCompatibleWithThisExpoGo(updateToOpen); - if (isCompatibleWithThisExpoGo) { - openUpdateManifestPermalink(updateToOpen); - } else { - navigation.push('ProjectDetails', { id }); - } - } else { - navigation.push('ProjectDetails', { id }); - } - } else { - navigation.push('ProjectDetails', { id }); - } - } - - const showSubtitle = subtitle && name.toLowerCase() !== subtitle.toLowerCase(); - - return ( - - - - - - - - - {name} - - {showSubtitle ? ( - <> - - - {subtitle} - - - ) : null} - - - - - - - - ); -} - -const styles = StyleSheet.create({ - titleText: { - fontSize: 15, - ...Platform.select({ - ios: { - fontWeight: '500', - }, - android: { - fontWeight: '400', - marginTop: 1, - }, - }), - }, -}); diff --git a/apps/expo-go/src/components/QRFooterButton.tsx b/apps/expo-go/src/components/QRFooterButton.tsx deleted file mode 100644 index b3e8035016c271..00000000000000 --- a/apps/expo-go/src/components/QRFooterButton.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// note(bacon): Purposefully skip using the themed icons since we want the icons to change color based on toggle state. -import Ionicons from '@expo/vector-icons/build/Ionicons'; -import { BlurView } from 'expo-blur'; -import * as Haptics from 'expo-haptics'; -import React from 'react'; -import { StyleSheet } from 'react-native'; -// @ts-expect-error -import TouchableBounce from 'react-native/Libraries/Components/Touchable/TouchableBounce'; - -import Colors from '../constants/Colors'; - -const size = 64; -const slop = 40; - -const hitSlop = { top: slop, bottom: slop, right: slop, left: slop }; - -export default function QRFooterButton({ - onPress, - isActive = false, - iconName, - iconSize = 36, -}: { - onPress: () => void; - isActive?: boolean; - iconName: React.ComponentProps['name']; - iconSize?: number; -}) { - const tint = isActive ? 'default' : 'dark'; - const iconColor = isActive ? Colors.light.tintColor : '#ffffff'; - - const onPressIn = React.useCallback(() => { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - }, []); - - const onPressButton = React.useCallback(() => { - onPress(); - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - }, [onPress]); - - return ( - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - width: size, - height: size, - overflow: 'hidden', - borderRadius: size / 2, - justifyContent: 'center', - alignItems: 'center', - }, -}); diff --git a/apps/expo-go/src/components/QRIndicator.tsx b/apps/expo-go/src/components/QRIndicator.tsx deleted file mode 100644 index 7856097cfcd1ec..00000000000000 --- a/apps/expo-go/src/components/QRIndicator.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { Animated, Easing, StyleSheet } from 'react-native'; -import Svg, { Path, SvgProps } from 'react-native-svg'; - -export default function QRIndicator() { - const scale = React.useMemo(() => new Animated.Value(1), []); - const duration = 500; - React.useEffect(() => { - let mounted = true; - - function cycleAnimation() { - Animated.sequence([ - Animated.timing(scale, { - easing: Easing.in(Easing.quad), - toValue: 1, - duration, - useNativeDriver: true, - }), - Animated.timing(scale, { - easing: Easing.out(Easing.quad), - toValue: 1.05, - duration, - useNativeDriver: true, - }), - ]).start(() => { - if (mounted) { - cycleAnimation(); - } - }); - } - cycleAnimation(); - return () => { - mounted = false; - }; - }, []); - - return ( - - ); -} - -// TODO(Bacon): Convert to functional after RN 63 upgrade. -class SvgComponent extends React.Component { - render() { - return ( - - - - ); - } -} - -const AnimatedScanner = Animated.createAnimatedComponent(SvgComponent); - -const styles = StyleSheet.create({ - scanner: { - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 1, - }, - shadowOpacity: 0.22, - shadowRadius: 2.22, - }, -}); diff --git a/apps/expo-go/src/components/RefreshControl.tsx b/apps/expo-go/src/components/RefreshControl.tsx deleted file mode 100644 index 6574b60b30844e..00000000000000 --- a/apps/expo-go/src/components/RefreshControl.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { RefreshControl as RNRefreshControl, Platform } from 'react-native'; -import { createNativeWrapper } from 'react-native-gesture-handler'; - -import Colors from '../constants/Colors'; - -type Props = React.ComponentProps; - -const RNGHRefreshControl = createNativeWrapper(RNRefreshControl, { - disallowInterruption: true, - shouldCancelWhenOutside: false, -}); - -const RefreshControl = Platform.OS === 'android' ? RNRefreshControl : RNGHRefreshControl; - -export default function StyledRefreshControl(props: Props) { - const theme = useTheme(); - const color = theme.dark ? Colors.dark.refreshControl : Colors.light.refreshControl; - - return ; -} diff --git a/apps/expo-go/src/components/SectionHeader.tsx b/apps/expo-go/src/components/SectionHeader.tsx deleted file mode 100644 index a4b00770abfcd4..00000000000000 --- a/apps/expo-go/src/components/SectionHeader.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { Heading, Row } from 'expo-dev-client-components'; -import * as React from 'react'; -import { StyleProp, ViewStyle } from 'react-native'; - -type Props = { - header: string; - style?: StyleProp; -}; - -export function SectionHeader({ header, style }: Props) { - return ( - - - {header} - - - ); -} diff --git a/apps/expo-go/src/components/ShareProjectButton.tsx b/apps/expo-go/src/components/ShareProjectButton.tsx deleted file mode 100644 index 2d7e26c4aad6cb..00000000000000 --- a/apps/expo-go/src/components/ShareProjectButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { Share, StyleSheet, TouchableOpacity } from 'react-native'; - -import * as Icons from './Icons'; -import Config from '../api/Config'; -import * as UrlUtils from '../utils/UrlUtils'; - -export default function ShareProjectButton( - props: Partial> & { - fullName: string; - } -) { - const theme = useExpoTheme(); - const onPress = React.useCallback(() => { - const url = `exp://${Config.api.host}/${props.fullName}`; - const message = UrlUtils.normalizeUrl(url); - Share.share({ - title: url, - message, - url: message, - }); - }, [props.fullName]); - - return ( - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingRight: 15, - }, -}); diff --git a/apps/expo-go/src/components/SnacksListItem.tsx b/apps/expo-go/src/components/SnacksListItem.tsx deleted file mode 100644 index 8075174621644f..00000000000000 --- a/apps/expo-go/src/components/SnacksListItem.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { ChevronDownIcon } from '@expo/styleguide-native'; -import { Row, Spacer, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import React from 'react'; -import { Alert, Linking } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import Environment from '../utils/Environment'; -import * as UrlUtils from '../utils/UrlUtils'; - -type Props = { - id: string; - name: string; - fullName: string; - description?: string; - isDraft: boolean; - first: boolean; - last: boolean; - sdkVersion: string; -}; - -function normalizeDescription(description?: string): string | undefined { - return !description || description === 'No description' ? undefined : description; -} - -/** - * This component is used to render a list item for the snacks section on the homescreen and on - * the snacks list page for an account. - */ - -export function SnacksListItem(snackData: Props) { - const { fullName, name, description, isDraft, first, last, sdkVersion } = snackData; - const theme = useExpoTheme(); - - const normalizedDescription = normalizeDescription(description); - const isSupported = Environment.isSupportedSdkVersion(sdkVersion); - - const handlePressProject = () => { - if (isSupported) { - Linking.openURL(UrlUtils.normalizeSnackUrl(fullName)); - } else { - const expoGoMajorVersion = Environment.supportedSdksString?.split('.')[0]; - const snackMajorVersion = sdkVersion?.split('.')[0]; - if (expoGoMajorVersion && snackMajorVersion) { - Alert.alert( - `Selected Snack uses unsupported SDK (${snackMajorVersion})`, - `The currently running version of Expo Go supports SDK ${expoGoMajorVersion} only. Update your Snack to this version to run it.` - ); - } else { - // Unlikely to hit this it's a good fallback case if we somehow get - // invalid data from the Snack or environment - Alert.alert( - `Selected Snack uses unsupported SDK`, - `Update your Snack to a compatible version to run it.` - ); - } - } - }; - - return ( - - - - - - - {name} - - {normalizedDescription && ( - <> - - - {normalizedDescription} - - - )} - {!isSupported && ( - <> - - - - Unsupported SDK ({sdkVersion}) - - - - )} - {isDraft && ( - <> - - - - Draft - - - - )} - - - - - - - ); -} diff --git a/apps/expo-go/src/components/Text.tsx b/apps/expo-go/src/components/Text.tsx deleted file mode 100644 index 717672a84f4bfa..00000000000000 --- a/apps/expo-go/src/components/Text.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { Text } from 'react-native'; - -import Colors, { ColorTheme } from '../constants/Colors'; - -type TextProps = Text['props']; -interface Props extends TextProps { - lightColor?: string; - darkColor?: string; -} - -type ThemedColors = keyof typeof Colors.light & keyof typeof Colors.dark; - -function useThemeColor(props: Props, colorName: ThemedColors) { - const theme = useTheme(); - const themeName = theme.dark ? ColorTheme.DARK : ColorTheme.LIGHT; - const colorFromProps = themeName === ColorTheme.DARK ? props.darkColor : props.lightColor; - - if (colorFromProps) { - return colorFromProps; - } else { - return Colors[themeName][colorName]; - } -} - -export const StyledText = (props: Props) => { - const { style, ...otherProps } = props; - const color = useThemeColor(props, 'text'); - - return ; -}; diff --git a/apps/expo-go/src/components/ThemedStatusBar.tsx b/apps/expo-go/src/components/ThemedStatusBar.tsx deleted file mode 100644 index 5d745b1a2ff91c..00000000000000 --- a/apps/expo-go/src/components/ThemedStatusBar.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useTheme, useIsFocused } from '@react-navigation/native'; -import { useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { Platform, StatusBar } from 'react-native'; - -export default function ThemedStatusBar() { - const theme = useTheme(); - const isFocused = useIsFocused(); - const expoTheme = useExpoTheme(); - - const barStyle = theme.dark ? 'light-content' : 'dark-content'; - - // When switching from an Expo project back to home sometimes the status bar will be - // changed back to the default status bar. This resolves that issue, but is messy. - if (Platform.OS === 'android' && isFocused) { - StatusBar.setBarStyle(barStyle); - } - - return ; -} diff --git a/apps/expo-go/src/components/UpdateListItem.tsx b/apps/expo-go/src/components/UpdateListItem.tsx deleted file mode 100644 index 6fe2059cdb6e1a..00000000000000 --- a/apps/expo-go/src/components/UpdateListItem.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { UpdateIcon, iconSize, ChevronDownIcon } from '@expo/styleguide-native'; -import format from 'date-fns/format'; -import { Row, Spacer, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import { BranchDetailsQuery } from 'src/graphql/types'; -import { - isUpdateCompatibleWithThisExpoGo, - openUpdateManifestPermalink, -} from 'src/utils/UpdateUtils'; - -import { DateFormats } from '../constants/DateFormats'; - -type Props = { - update: NonNullable['updates'][0]; - first: boolean; - last: boolean; -}; - -export function UpdateListItem({ update, first, last }: Props) { - const { id, message, createdAt } = update; - - const isCompatibleWithThisExpoGo = isUpdateCompatibleWithThisExpoGo(update); - - const theme = useExpoTheme(); - - const handlePress = () => { - openUpdateManifestPermalink(update); - }; - - return ( - - - - - - - - - - - {message ? `"${message}"` : id} - - - - Published {format(new Date(createdAt), DateFormats.timestamp)} - - {!isCompatibleWithThisExpoGo && ( - <> - - - Not compatible with this version of Expo Go - - - )} - - - - - {isCompatibleWithThisExpoGo && ( - - )} - - - - - ); -} diff --git a/apps/expo-go/src/components/UserReviewSection.tsx b/apps/expo-go/src/components/UserReviewSection.tsx deleted file mode 100644 index 2dfd46bde34ca9..00000000000000 --- a/apps/expo-go/src/components/UserReviewSection.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { iconSize, XIcon, spacing, typography } from '@expo/styleguide-native'; -import { Row, Text, useExpoTheme, View, Button, Spacer } from 'expo-dev-client-components'; -import { isDevice } from 'expo-device'; -import React from 'react'; -import { StyleSheet, TouchableOpacity } from 'react-native'; - -import { CommonAppDataFragment, CommonSnackDataFragment } from '../graphql/types'; -import { useUserReviewCheck } from '../utils/useUserReviewCheck'; - -type Props = { - apps?: CommonAppDataFragment[]; - snacks?: CommonSnackDataFragment[]; -}; - -export default function UserReviewSection({ snacks, apps }: Props) { - const { shouldShowReviewSection, requestStoreReview, dismissReviewSection, provideFeedback } = - useUserReviewCheck({ - apps, - snacks, - }); - const theme = useExpoTheme(); - - if ((!isDevice && !__DEV__) || !shouldShowReviewSection) { - return null; - } - - return ( - <> - - - - - - - Enjoying Expo Go? - - - Whether you love the app or feel we could be doing better, let us know! Your feedback - will help us improve. - - - - - Not really - - - - - Love it! - - - - - - - - ); -} - -const styles = StyleSheet.create({ - dismissButton: { - position: 'absolute', - top: spacing[4], - right: spacing[4], - zIndex: 1, - }, - title: { - marginBottom: spacing[2], - }, - subtitle: { - marginBottom: spacing[2], - }, -}); diff --git a/apps/expo-go/src/components/Views.tsx b/apps/expo-go/src/components/Views.tsx deleted file mode 100644 index c24f718656143e..00000000000000 --- a/apps/expo-go/src/components/Views.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { ScrollViewProps, useWindowDimensions, View, ViewStyle } from 'react-native'; -import { ScrollView } from 'react-native-gesture-handler'; - -import Colors, { ColorTheme } from '../constants/Colors'; - -type ViewProps = View['props']; - -interface Props extends ViewProps { - lightBackgroundColor?: string; - darkBackgroundColor?: string; - lightBorderColor?: string; - darkBorderColor?: string; -} - -interface StyledScrollViewProps extends ScrollViewProps { - lightBackgroundColor?: string; - darkBackgroundColor?: string; -} - -type ThemedColors = keyof typeof Colors.light & keyof typeof Colors.dark; - -function useThemeName(): ColorTheme { - const theme = useTheme(); - return theme.dark ? ColorTheme.DARK : ColorTheme.LIGHT; -} - -function useThemeBackgroundColor(props: Props | StyledScrollViewProps, colorName: ThemedColors) { - const themeName = useThemeName(); - const colorFromProps = - themeName === ColorTheme.DARK ? props.darkBackgroundColor : props.lightBackgroundColor; - - if (colorFromProps) { - return colorFromProps; - } else { - return Colors[themeName][colorName]; - } -} - -function useThemeBorderColor(props: Props, colorName: ThemedColors) { - const themeName = useThemeName(); - const colorFromProps = - themeName === ColorTheme.DARK ? props.darkBorderColor : props.lightBorderColor; - - if (colorFromProps) { - return colorFromProps; - } else { - return Colors[themeName][colorName]; - } -} - -export const StyledScrollView = React.forwardRef( - (props: StyledScrollViewProps, ref?: React.Ref) => { - const { style, ...otherProps } = props; - const backgroundColor = useThemeBackgroundColor(props, 'absolute'); - - // @ts-expect-error until react-native-gesture-handler fixes the type - return ; - } -); - -export const StyledView = (props: Props) => { - const { - style, - lightBackgroundColor: _lightBackgroundColor, - darkBackgroundColor: _darkBackgroundColor, - lightBorderColor: _lightBorderColor, - darkBorderColor: _darkBorderColor, - ...otherProps - } = props; - - const backgroundColor = useThemeBackgroundColor(props, 'cardBackground'); - const borderColor = useThemeBorderColor(props, 'cardSeparator'); - - return ( - - ); -}; - -/** - * View used to limit the content width to stay at most as wide as the screen is wide. - * Usually this makes the content look better in landscape mode. - */ -export function CappedWidthContainerView( - props: ViewProps & { wrapperStyle?: ViewStyle | ViewStyle[] } -) { - const { height: screenHeight } = useWindowDimensions(); - return ( - - - - ); -} diff --git a/apps/expo-go/src/constants/Colors.ts b/apps/expo-go/src/constants/Colors.ts deleted file mode 100644 index 24c9a95973b2b5..00000000000000 --- a/apps/expo-go/src/constants/Colors.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { lightTheme, darkTheme } from '@expo/styleguide-native'; -import { Platform } from 'react-native'; - -export enum ColorTheme { - LIGHT = 'light', - DARK = 'dark', -} - -const tintColor = '#4e9bde'; -const darkTintColor = '#1a74b3'; -const error = '#dc3545'; - -export default { - [ColorTheme.LIGHT]: { - absolute: '#fff', - text: '#242c39', - tintColor, - darkTintColor, - navBorderBottom: 'rgba(46, 59, 76, 0.10)', - navBackgroundColor: '#fff', - sectionLabelBackgroundColor: '#f8f8f9', - sectionLabelText: '#a7aab0', - bodyBackground: lightTheme.background.screen, - cardBackground: '#fff', - cardSeparator: '#f4f4f5', - cardTitle: '#242c39', - error, - highlightColor: '#5944ed', - noticeText: '#fff', - greyBackground: '#f8f8f9', - greyText: '#a7aab0', - greyUnderlayColor: '#f7f7f7', - blackText: '#242c39', - separator: '#f4f4f5', - refreshControl: undefined, - - /** - * Note: These colors are not actually used by the tab bar currently! - * See: BottomTabNavigator.ts - */ - tabIconDefault: '#bdbfc3', - tabIconSelected: Platform.OS === 'android' ? '#000' : tintColor, - tabBar: '#fff', - }, - [ColorTheme.DARK]: { - absolute: '#000', - text: '#fff', - tintColor: darkTintColor, - darkTintColor: tintColor, - navBackgroundColor: '#000', - navBorderBottom: '#000', - sectionLabelBackgroundColor: '#2a2a2a', - sectionLabelText: '#fff', - bodyBackground: darkTheme.background.screen, - cardBackground: '#1c1c1e', - cardSeparator: '#343437', - cardTitle: '#fff', - separator: '#1b1b1b', - error, - highlightColor: '#5944ed', - noticeText: '#fff', - greyBackground: '#f8f8f9', - greyText: '#a7aab0', - greyUnderlayColor: '#f7f7f7', - blackText: '#242c39', - refreshControl: '#ffffff', - - /** - * Note: These colors are not actually used by the tab bar currently! - * See: BottomTabNavigator.ts - */ - tabBar: '#000', - tabIconDefault: '#bdbfc3', - tabIconSelected: Platform.OS === 'android' ? '#fff' : tintColor, - }, -}; diff --git a/apps/expo-go/src/constants/DateFormats.ts b/apps/expo-go/src/constants/DateFormats.ts deleted file mode 100644 index 6b797acc750b45..00000000000000 --- a/apps/expo-go/src/constants/DateFormats.ts +++ /dev/null @@ -1,9 +0,0 @@ -// https://date-fns.org/v2.11.0/docs/format -export const DateFormats = { - timestamp: 'MMM d, yyyy h:mm a', - extendedTimestamp: 'EEEE, MMM d, yyyy h:mm a', - date: 'MMM d, yyyy', - dateWithDay: 'EEEE MMM d, yyyy', - dayOfMonth: 'do', - timeOfDay: 'h:mm a', -}; diff --git a/apps/expo-go/src/constants/FormStates.ts b/apps/expo-go/src/constants/FormStates.ts deleted file mode 100644 index f92028ecfced14..00000000000000 --- a/apps/expo-go/src/constants/FormStates.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum FormStates { - IDLE = 'idle', - LOADING = 'loading', - SUCCESS = 'success', - ERRORED = 'errored', -} diff --git a/apps/expo-go/src/constants/SharedStyles.ts b/apps/expo-go/src/constants/SharedStyles.ts deleted file mode 100644 index 8c989d61ed3846..00000000000000 --- a/apps/expo-go/src/constants/SharedStyles.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Platform, StyleSheet } from 'react-native'; - -import Colors from './Colors'; - -export default StyleSheet.create({ - sectionLabelContainer: { - flexDirection: 'row', - paddingVertical: 10, - alignItems: 'center', - paddingHorizontal: 15, - backgroundColor: Colors.light.greyBackground, - }, - sectionLabelText: { - color: Colors.light.greyText, - letterSpacing: 0.92, - ...Platform.select({ - ios: { - fontWeight: '500', - fontSize: 11, - }, - android: { - fontWeight: '400', - fontSize: 12, - }, - }), - }, - regularText: { - color: Colors.light.blackText, - fontSize: 13, - }, - faintText: { - color: Colors.light.greyText, - fontSize: 13, - }, - noticeTitleText: { - color: '#232b3a', - marginBottom: 15, - fontWeight: '400', - ...Platform.select({ - ios: { - fontSize: 22, - }, - android: { - fontSize: 23, - }, - }), - }, - noticeDescriptionText: { - textAlign: 'center', - marginBottom: 20, - ...Platform.select({ - ios: { - fontSize: 15, - lineHeight: 20, - marginHorizontal: 10, - }, - android: { - fontSize: 16, - lineHeight: 24, - marginHorizontal: 15, - }, - }), - }, - genericCardContainer: { - backgroundColor: '#fff', - flexGrow: 1, - borderBottomColor: Colors.light.separator, - borderBottomWidth: StyleSheet.hairlineWidth * 2, - }, - genericCardBody: { - paddingTop: 20, - paddingLeft: 15, - paddingRight: 10, - paddingBottom: 17, - }, - genericCardDescriptionContainer: { - paddingHorizontal: 15, - paddingTop: 10, - }, - genericCardDescriptionText: { - color: Colors.light.greyText, - fontSize: 13, - }, - genericCardTitle: { - color: Colors.light.blackText, - fontSize: 16, - marginRight: 50, - marginBottom: 2, - fontWeight: '400', - }, -}); diff --git a/apps/expo-go/src/constants/Themes.ts b/apps/expo-go/src/constants/Themes.ts deleted file mode 100644 index 306a8dc1bed574..00000000000000 --- a/apps/expo-go/src/constants/Themes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DarkTheme, DefaultTheme } from '@react-navigation/native'; - -import Colors, { ColorTheme } from './Colors'; - -export default { - [ColorTheme.LIGHT]: { - ...DefaultTheme, - colors: { - ...DefaultTheme.colors, - text: Colors.light.text, - card: Colors.light.tabBar, - border: Colors.light.navBorderBottom, - primary: Colors.light.tintColor, - }, - }, - [ColorTheme.DARK]: { - ...DarkTheme, - colors: { - ...DarkTheme.colors, - text: Colors.dark.text, - card: Colors.dark.tabBar, - border: Colors.dark.navBorderBottom, - primary: Colors.dark.tintColor, - }, - }, -}; diff --git a/apps/expo-go/src/graphql/fragments/CommonAppData.fragment.graphql b/apps/expo-go/src/graphql/fragments/CommonAppData.fragment.graphql deleted file mode 100644 index b213e1d71fb6ce..00000000000000 --- a/apps/expo-go/src/graphql/fragments/CommonAppData.fragment.graphql +++ /dev/null @@ -1,16 +0,0 @@ -fragment CommonAppData on App { - id - name - fullName - ownerAccount { - name - } - firstTwoBranches: updateBranches(limit: 2, offset: 0) { - id - name - updates(limit: 1, offset: 0, filter: { platform: $platform }) { - id - ...UpdateData - } - } -} \ No newline at end of file diff --git a/apps/expo-go/src/graphql/fragments/CommonSnackData.fragment.graphql b/apps/expo-go/src/graphql/fragments/CommonSnackData.fragment.graphql deleted file mode 100644 index 1caaab3855f2fe..00000000000000 --- a/apps/expo-go/src/graphql/fragments/CommonSnackData.fragment.graphql +++ /dev/null @@ -1,9 +0,0 @@ -fragment CommonSnackData on Snack { - id - name - description - fullName - slug - isDraft - sdkVersion -} \ No newline at end of file diff --git a/apps/expo-go/src/graphql/fragments/CurrentUserActorData.fragment.graphql b/apps/expo-go/src/graphql/fragments/CurrentUserActorData.fragment.graphql deleted file mode 100644 index 9b3a5915009ca6..00000000000000 --- a/apps/expo-go/src/graphql/fragments/CurrentUserActorData.fragment.graphql +++ /dev/null @@ -1,21 +0,0 @@ -fragment CurrentUserActorData on UserActor { - __typename - id - username - firstName - lastName - profilePhoto - bestContactEmail - accounts { - id - name - ownerUserActor { - id - username - profilePhoto - firstName - fullName - lastName - } - } -} diff --git a/apps/expo-go/src/graphql/fragments/UpdateData.fragment.graphql b/apps/expo-go/src/graphql/fragments/UpdateData.fragment.graphql deleted file mode 100644 index cd49fe7890ba73..00000000000000 --- a/apps/expo-go/src/graphql/fragments/UpdateData.fragment.graphql +++ /dev/null @@ -1,10 +0,0 @@ -fragment UpdateData on Update { - id - group - message - createdAt - runtimeVersion - expoGoSDKVersion - platform - manifestPermalink -} \ No newline at end of file diff --git a/apps/expo-go/src/graphql/queries/AccountDataQuery.query.graphql b/apps/expo-go/src/graphql/queries/AccountDataQuery.query.graphql deleted file mode 100644 index d4ef76a83511df..00000000000000 --- a/apps/expo-go/src/graphql/queries/AccountDataQuery.query.graphql +++ /dev/null @@ -1,15 +0,0 @@ -query Home_AccountData($accountName: String!, $appLimit: Int!, $snackLimit: Int!, $platform: AppPlatform!) { - account { - byName(accountName: $accountName) { - id - name - appCount - apps(limit: $appLimit, offset: 0, includeUnpublished: true) { - ...CommonAppData - } - snacks(limit: $snackLimit, offset: 0) { - ...CommonSnackData - } - } - } -} \ No newline at end of file diff --git a/apps/expo-go/src/graphql/queries/BranchDetails.query.graphql b/apps/expo-go/src/graphql/queries/BranchDetails.query.graphql deleted file mode 100644 index 13d6db9c4c7d8f..00000000000000 --- a/apps/expo-go/src/graphql/queries/BranchDetails.query.graphql +++ /dev/null @@ -1,22 +0,0 @@ -query BranchDetails( - $name: String! - $appId: String! - $platform: AppPlatform! -) { - app { - byId(appId: $appId) { - id - name - slug - fullName - updateBranchByName(name: $name) { - id - name - updates(limit: 100, offset: 0, filter: { platform: $platform }) { - id - ...UpdateData - } - } - } - } -} diff --git a/apps/expo-go/src/graphql/queries/BranchesForProjectQuery.query.graphql b/apps/expo-go/src/graphql/queries/BranchesForProjectQuery.query.graphql deleted file mode 100644 index f10276e7fe9d9d..00000000000000 --- a/apps/expo-go/src/graphql/queries/BranchesForProjectQuery.query.graphql +++ /dev/null @@ -1,22 +0,0 @@ -query BranchesForProject( - $appId: String! - $platform: AppPlatform! - $limit: Int! - $offset: Int! -) { - app { - byId(appId: $appId) { - id - name - fullName - updateBranches(limit: $limit, offset: $offset) { - id - name - updates(limit: 1, offset: 0, filter: { platform: $platform }) { - id - ...UpdateData - } - } - } - } -} diff --git a/apps/expo-go/src/graphql/queries/CurrentUserActorQuery.query.graphql b/apps/expo-go/src/graphql/queries/CurrentUserActorQuery.query.graphql deleted file mode 100644 index 19e408c9dfe7db..00000000000000 --- a/apps/expo-go/src/graphql/queries/CurrentUserActorQuery.query.graphql +++ /dev/null @@ -1,5 +0,0 @@ -query Home_CurrentUserActor { - meUserActor { - ...CurrentUserActorData - } -} diff --git a/apps/expo-go/src/graphql/queries/ProjectsListQuery.query.graphql b/apps/expo-go/src/graphql/queries/ProjectsListQuery.query.graphql deleted file mode 100644 index 94b55d5f49d5b4..00000000000000 --- a/apps/expo-go/src/graphql/queries/ProjectsListQuery.query.graphql +++ /dev/null @@ -1,11 +0,0 @@ -query Home_AccountApps($accountName: String!, $limit: Int!, $offset: Int!, $platform: AppPlatform!) { - account { - byName(accountName: $accountName) { - id - appCount - apps(limit: $limit, offset: $offset, includeUnpublished: true) { - ...CommonAppData - } - } - } -} \ No newline at end of file diff --git a/apps/expo-go/src/graphql/queries/ProjectsQuery.query.graphql b/apps/expo-go/src/graphql/queries/ProjectsQuery.query.graphql deleted file mode 100644 index 9f9bf13602bf47..00000000000000 --- a/apps/expo-go/src/graphql/queries/ProjectsQuery.query.graphql +++ /dev/null @@ -1,24 +0,0 @@ -query ProjectsQuery( - $appId: String! - $platform: AppPlatform! -) { - app { - byId(appId: $appId) { - id - name - slug - fullName - ownerAccount { - name - } - updateBranches(limit: 100, offset: 0) { - id - name - updates(limit: 1, offset: 0, filter: { platform: $platform }) { - id - ...UpdateData - } - } - } - } -} diff --git a/apps/expo-go/src/graphql/queries/SnacksListQuery.query.graphql b/apps/expo-go/src/graphql/queries/SnacksListQuery.query.graphql deleted file mode 100644 index 0dc6139af8b977..00000000000000 --- a/apps/expo-go/src/graphql/queries/SnacksListQuery.query.graphql +++ /dev/null @@ -1,11 +0,0 @@ -query Home_AccountSnacks($accountName: String!, $limit: Int!, $offset: Int!) { - account { - byName(accountName: $accountName) { - id - name - snacks(limit: $limit, offset: $offset) { - ...CommonSnackData - } - } - } -} \ No newline at end of file diff --git a/apps/expo-go/src/graphql/queries/ViewerPrimaryAccountNameQuery.query.graphql b/apps/expo-go/src/graphql/queries/ViewerPrimaryAccountNameQuery.query.graphql deleted file mode 100644 index a76eabe1e07bca..00000000000000 --- a/apps/expo-go/src/graphql/queries/ViewerPrimaryAccountNameQuery.query.graphql +++ /dev/null @@ -1,9 +0,0 @@ -query Home_ViewerPrimaryAccountName { - meUserActor { - id - primaryAccount { - id - name - } - } -} \ No newline at end of file diff --git a/apps/expo-go/src/graphql/types.ts b/apps/expo-go/src/graphql/types.ts deleted file mode 100644 index b1e6f1c5a2083d..00000000000000 --- a/apps/expo-go/src/graphql/types.ts +++ /dev/null @@ -1,8782 +0,0 @@ -import { gql } from '@apollo/client'; -import * as Apollo from '@apollo/client'; -export type Maybe = T | null; -export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -const defaultOptions = {} as const; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: string; - String: string; - Boolean: boolean; - Int: number; - Float: number; - DateTime: any; - DevDomainName: any; - JSON: any; - JSONObject: any; - WorkerDeploymentIdentifier: any; -}; - -export type AcceptUserInvitationResult = { - __typename?: 'AcceptUserInvitationResult'; - success: Scalars['Boolean']; -}; - -/** A method of authentication for an Actor */ -export type AccessToken = { - __typename?: 'AccessToken'; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - lastUsedAt?: Maybe; - note?: Maybe; - owner: Actor; - revokedAt?: Maybe; - updatedAt: Scalars['DateTime']; - visibleTokenPrefix: Scalars['String']; -}; - -export type AccessTokenMutation = { - __typename?: 'AccessTokenMutation'; - /** Create an AccessToken for an Actor */ - createAccessToken: CreateAccessTokenResponse; - /** Delete an AccessToken */ - deleteAccessToken: DeleteAccessTokenResult; - /** Revoke an AccessToken */ - setAccessTokenRevoked: AccessToken; -}; - - -export type AccessTokenMutationCreateAccessTokenArgs = { - createAccessTokenData: CreateAccessTokenInput; -}; - - -export type AccessTokenMutationDeleteAccessTokenArgs = { - id: Scalars['ID']; -}; - - -export type AccessTokenMutationSetAccessTokenRevokedArgs = { - id: Scalars['ID']; - revoked?: InputMaybe; -}; - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type Account = { - __typename?: 'Account'; - /** @deprecated Legacy access tokens are deprecated */ - accessTokens: Array>; - /** Coalesced project activity for all apps belonging to this account. */ - activityTimelineProjectActivities: Array; - appCount: Scalars['Int']; - /** @deprecated Use appStoreConnectApiKeysPaginated */ - appStoreConnectApiKeys: Array; - appStoreConnectApiKeysPaginated: AccountAppStoreConnectApiKeysConnection; - appleAppIdentifiers: Array; - /** @deprecated Use appleDevicesPaginated */ - appleDevices: Array; - appleDevicesPaginated: AccountAppleDevicesConnection; - /** @deprecated Use appleDistributionCertificatesPaginated */ - appleDistributionCertificates: Array; - appleDistributionCertificatesPaginated: AccountAppleDistributionCertificatesConnection; - /** @deprecated Use appleProvisioningProfilesPaginated */ - appleProvisioningProfiles: Array; - appleProvisioningProfilesPaginated: AccountAppleProvisioningProfilesConnection; - /** @deprecated Use applePushKeysPaginated */ - applePushKeys: Array; - applePushKeysPaginated: AccountApplePushKeysConnection; - /** @deprecated Use appleTeamsPaginated */ - appleTeams: Array; - /** iOS credentials for account */ - appleTeamsPaginated: AccountAppleTeamsConnection; - /** - * Apps associated with this account - * @deprecated Use appsPaginated - */ - apps: Array; - /** Paginated list of apps associated with this account. By default sorted by name. Use filter to adjust the sorting order. */ - appsPaginated: AccountAppsConnection; - /** Audit logs for account */ - auditLogsPaginated: AuditLogConnection; - /** @deprecated Build packs are no longer supported */ - availableBuilds?: Maybe; - /** Billing information. Only visible to members with the ADMIN or OWNER role. */ - billing?: Maybe; - billingPeriod: BillingPeriod; - /** (EAS Build) Builds associated with this account */ - builds: Array; - createdAt: Scalars['DateTime']; - /** Environment secrets for an account */ - environmentSecrets: Array; - /** Environment variables for an account */ - environmentVariables: Array; - /** Environment variables for an account with decrypted secret values */ - environmentVariablesIncludingSensitive: Array; - /** GitHub App installations for an account */ - githubAppInstallations: Array; - /** @deprecated Use googleServiceAccountKeysPaginated */ - googleServiceAccountKeys: Array; - /** Android credentials for account */ - googleServiceAccountKeysPaginated: AccountGoogleServiceAccountKeysConnection; - id: Scalars['ID']; - isCurrent: Scalars['Boolean']; - isDisabled: Scalars['Boolean']; - /** Whether this account has SSO enabled. Can be queried by all members. */ - isSSOEnabled: Scalars['Boolean']; - lastDeletionAttemptTime?: Maybe; - name: Scalars['String']; - /** Offers set on this account */ - offers?: Maybe>; - /** Owning User of this account if personal account */ - owner?: Maybe; - /** Owning UserActor of this account if personal account */ - ownerUserActor?: Maybe; - pushSecurityEnabled: Scalars['Boolean']; - /** @deprecated Legacy access tokens are deprecated */ - requiresAccessTokenForPushSecurity: Scalars['Boolean']; - /** Snacks associated with this account */ - snacks: Array; - /** SSO configuration for this account */ - ssoConfiguration?: Maybe; - /** Subscription info visible to members that have VIEWER role */ - subscription?: Maybe; - /** @deprecated No longer needed */ - subscriptionChangesPending?: Maybe; - /** Coalesced project activity for an app using pagination */ - timelineActivity: TimelineActivityConnection; - /** @deprecated See isCurrent */ - unlimitedBuilds: Scalars['Boolean']; - updatedAt: Scalars['DateTime']; - /** Account query object for querying EAS usage metrics */ - usageMetrics: AccountUsageMetrics; - /** - * Owning UserActor of this account if personal account - * @deprecated Deprecated in favor of ownerUserActor - */ - userActorOwner?: Maybe; - /** Pending user invitations for this account */ - userInvitations: Array; - /** Actors associated with this account and permissions they hold */ - users: Array; - /** Permission info for the viewer on this account */ - viewerUserPermission: UserPermission; - /** @deprecated Build packs are no longer supported */ - willAutoRenewBuilds?: Maybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountActivityTimelineProjectActivitiesArgs = { - createdBefore?: InputMaybe; - filterTypes?: InputMaybe>; - limit: Scalars['Int']; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppStoreConnectApiKeysPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleAppIdentifiersArgs = { - bundleIdentifier?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleDevicesArgs = { - identifier?: InputMaybe; - limit?: InputMaybe; - offset?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleDevicesPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleDistributionCertificatesPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleProvisioningProfilesArgs = { - appleAppIdentifierId?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleProvisioningProfilesPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountApplePushKeysPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleTeamsArgs = { - appleTeamIdentifier?: InputMaybe; - limit?: InputMaybe; - offset?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppleTeamsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppsArgs = { - includeUnpublished?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAppsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountAuditLogsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountBillingPeriodArgs = { - date: Scalars['DateTime']; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountBuildsArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; - platform?: InputMaybe; - status?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountEnvironmentSecretsArgs = { - filterNames?: InputMaybe>; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountEnvironmentVariablesArgs = { - environment?: InputMaybe; - filterNames?: InputMaybe>; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountEnvironmentVariablesIncludingSensitiveArgs = { - environment?: InputMaybe; - filterNames?: InputMaybe>; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountGoogleServiceAccountKeysPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountSnacksArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** - * An account is a container owning projects, credentials, billing and other organization - * data and settings. Actors may own and be members of accounts. - */ -export type AccountTimelineActivityArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type AccountAppStoreConnectApiKeysConnection = { - __typename?: 'AccountAppStoreConnectApiKeysConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountAppStoreConnectApiKeysEdge = { - __typename?: 'AccountAppStoreConnectApiKeysEdge'; - cursor: Scalars['String']; - node: AppStoreConnectApiKey; -}; - -export type AccountAppleDevicesConnection = { - __typename?: 'AccountAppleDevicesConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountAppleDevicesEdge = { - __typename?: 'AccountAppleDevicesEdge'; - cursor: Scalars['String']; - node: AppleDevice; -}; - -export type AccountAppleDistributionCertificatesConnection = { - __typename?: 'AccountAppleDistributionCertificatesConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountAppleDistributionCertificatesEdge = { - __typename?: 'AccountAppleDistributionCertificatesEdge'; - cursor: Scalars['String']; - node: AppleDistributionCertificate; -}; - -export type AccountAppleProvisioningProfilesConnection = { - __typename?: 'AccountAppleProvisioningProfilesConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountAppleProvisioningProfilesEdge = { - __typename?: 'AccountAppleProvisioningProfilesEdge'; - cursor: Scalars['String']; - node: AppleProvisioningProfile; -}; - -export type AccountApplePushKeysConnection = { - __typename?: 'AccountApplePushKeysConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountApplePushKeysEdge = { - __typename?: 'AccountApplePushKeysEdge'; - cursor: Scalars['String']; - node: ApplePushKey; -}; - -export type AccountAppleTeamsConnection = { - __typename?: 'AccountAppleTeamsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountAppleTeamsEdge = { - __typename?: 'AccountAppleTeamsEdge'; - cursor: Scalars['String']; - node: AppleTeam; -}; - -export type AccountAppsConnection = { - __typename?: 'AccountAppsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountAppsEdge = { - __typename?: 'AccountAppsEdge'; - cursor: Scalars['String']; - node: App; -}; - -export type AccountAppsFilterInput = { - searchTerm?: InputMaybe; - sortByField: AccountAppsSortByField; -}; - -export enum AccountAppsSortByField { - LatestActivityTime = 'LATEST_ACTIVITY_TIME', - /** - * Name prefers the display name but falls back to full_name with @account/ - * part stripped. - */ - Name = 'NAME' -} - -export type AccountDataInput = { - name: Scalars['String']; -}; - -export type AccountGoogleServiceAccountKeysConnection = { - __typename?: 'AccountGoogleServiceAccountKeysConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AccountGoogleServiceAccountKeysEdge = { - __typename?: 'AccountGoogleServiceAccountKeysEdge'; - cursor: Scalars['String']; - node: GoogleServiceAccountKey; -}; - -export type AccountMutation = { - __typename?: 'AccountMutation'; - /** Cancels all subscriptions immediately */ - cancelAllSubscriptionsImmediately: Account; - /** Cancel scheduled subscription change */ - cancelScheduledSubscriptionChange: Account; - /** Buys or revokes account's additional concurrencies, charging the account the appropriate amount if needed. */ - changeAdditionalConcurrenciesCount: Account; - /** Upgrades or downgrades the active subscription to the newPlanIdentifier, which must be one of the EAS plans (i.e., Production or Enterprise). */ - changePlan: Account; - /** Add specified account Permissions for Actor. Actor must already have at least one permission on the account. */ - grantActorPermissions: Account; - /** Rename this account and the primary user's username if this account is a personal account */ - rename: Account; - /** Requests a refund for the specified charge by requesting a manual refund from support */ - requestRefund?: Maybe; - /** Revoke specified Permissions for Actor. Actor must already have at least one permission on the account. */ - revokeActorPermissions: Account; - /** Require authorization to send push notifications for experiences owned by this account */ - setPushSecurityEnabled: Account; -}; - - -export type AccountMutationCancelAllSubscriptionsImmediatelyArgs = { - accountID: Scalars['ID']; -}; - - -export type AccountMutationCancelScheduledSubscriptionChangeArgs = { - accountID: Scalars['ID']; -}; - - -export type AccountMutationChangeAdditionalConcurrenciesCountArgs = { - accountID: Scalars['ID']; - newAdditionalConcurrenciesCount: Scalars['Int']; -}; - - -export type AccountMutationChangePlanArgs = { - accountID: Scalars['ID']; - couponCode?: InputMaybe; - newPlanIdentifier: Scalars['String']; -}; - - -export type AccountMutationGrantActorPermissionsArgs = { - accountID: Scalars['ID']; - actorID: Scalars['ID']; - permissions?: InputMaybe>>; -}; - - -export type AccountMutationRenameArgs = { - accountID: Scalars['ID']; - newName: Scalars['String']; -}; - - -export type AccountMutationRequestRefundArgs = { - accountID: Scalars['ID']; - chargeID: Scalars['ID']; - description?: InputMaybe; - reason?: InputMaybe; -}; - - -export type AccountMutationRevokeActorPermissionsArgs = { - accountID: Scalars['ID']; - actorID: Scalars['ID']; - permissions?: InputMaybe>>; -}; - - -export type AccountMutationSetPushSecurityEnabledArgs = { - accountID: Scalars['ID']; - pushSecurityEnabled: Scalars['Boolean']; -}; - -export type AccountNotificationSubscriptionInput = { - accountId: Scalars['ID']; - event: NotificationEvent; - type: NotificationType; - userId: Scalars['ID']; -}; - -export type AccountQuery = { - __typename?: 'AccountQuery'; - /** Query an Account by ID */ - byId: Account; - /** Query an Account by name */ - byName: Account; -}; - - -export type AccountQueryByIdArgs = { - accountId: Scalars['String']; -}; - - -export type AccountQueryByNameArgs = { - accountName: Scalars['String']; -}; - -/** Auth configuration data for an SSO account. */ -export type AccountSsoConfiguration = { - __typename?: 'AccountSSOConfiguration'; - authProtocol: AuthProtocolType; - authProviderIdentifier: AuthProviderIdentifier; - clientIdentifier: Scalars['String']; - clientSecret: Scalars['String']; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - issuer: Scalars['String']; - updatedAt: Scalars['DateTime']; -}; - -export type AccountSsoConfigurationData = { - authProtocol: AuthProtocolType; - authProviderIdentifier: AuthProviderIdentifier; - clientIdentifier: Scalars['String']; - clientSecret: Scalars['String']; - issuer: Scalars['String']; -}; - -export type AccountSsoConfigurationMutation = { - __typename?: 'AccountSSOConfigurationMutation'; - /** Create an AccountSSOConfiguration for an Account */ - createAccountSSOConfiguration: AccountSsoConfiguration; - /** Delete an AccountSSOConfiguration */ - deleteAccountSSOConfiguration: DeleteAccountSsoConfigurationResult; - /** Update an AccountSSOConfiguration */ - updateAccountSSOConfiguration: AccountSsoConfiguration; -}; - - -export type AccountSsoConfigurationMutationCreateAccountSsoConfigurationArgs = { - accountId: Scalars['ID']; - accountSSOConfigurationData: AccountSsoConfigurationData; -}; - - -export type AccountSsoConfigurationMutationDeleteAccountSsoConfigurationArgs = { - id: Scalars['ID']; -}; - - -export type AccountSsoConfigurationMutationUpdateAccountSsoConfigurationArgs = { - accountSSOConfigurationData: AccountSsoConfigurationData; - id: Scalars['ID']; -}; - -/** Public auth configuration data for an SSO account. */ -export type AccountSsoConfigurationPublicData = { - __typename?: 'AccountSSOConfigurationPublicData'; - authProtocol: AuthProtocolType; - authProviderIdentifier: AuthProviderIdentifier; - authorizationUrl: Scalars['String']; - id: Scalars['ID']; - issuer: Scalars['String']; -}; - -export type AccountSsoConfigurationPublicDataQuery = { - __typename?: 'AccountSSOConfigurationPublicDataQuery'; - /** Get AccountSSOConfiguration public data by account name */ - publicDataByAccountName: AccountSsoConfigurationPublicData; -}; - - -export type AccountSsoConfigurationPublicDataQueryPublicDataByAccountNameArgs = { - accountName: Scalars['String']; -}; - -export enum AccountUploadSessionType { - WorkflowsProjectSources = 'WORKFLOWS_PROJECT_SOURCES' -} - -export type AccountUsageEasBuildMetadata = { - __typename?: 'AccountUsageEASBuildMetadata'; - billingResourceClass?: Maybe; - platform?: Maybe; - waiverType?: Maybe; -}; - -export type AccountUsageEasJobsMetadata = { - __typename?: 'AccountUsageEASJobsMetadata'; - resourceClassDisplayName: Scalars['String']; -}; - -export type AccountUsageMetadata = AccountUsageEasBuildMetadata | AccountUsageEasJobsMetadata; - -export type AccountUsageMetric = { - __typename?: 'AccountUsageMetric'; - id: Scalars['ID']; - metricType: UsageMetricType; - serviceMetric: EasServiceMetric; - timestamp: Scalars['DateTime']; - value: Scalars['Float']; -}; - -export type AccountUsageMetrics = { - __typename?: 'AccountUsageMetrics'; - byBillingPeriod: UsageMetricTotal; - metricsForServiceMetric: Array; -}; - - -export type AccountUsageMetricsByBillingPeriodArgs = { - date: Scalars['DateTime']; - service?: InputMaybe; -}; - - -export type AccountUsageMetricsMetricsForServiceMetricArgs = { - filterParams?: InputMaybe; - granularity: UsageMetricsGranularity; - serviceMetric: EasServiceMetric; - timespan: UsageMetricsTimespan; -}; - -export type ActivityTimelineProjectActivity = { - activityTimestamp: Scalars['DateTime']; - actor?: Maybe; - id: Scalars['ID']; -}; - -export enum ActivityTimelineProjectActivityType { - Build = 'BUILD', - Submission = 'SUBMISSION', - Update = 'UPDATE', - Worker = 'WORKER' -} - -/** A regular user, SSO user, or robot that can authenticate with Expo services and be a member of accounts. */ -export type Actor = { - /** Access Tokens belonging to this actor */ - accessTokens: Array; - /** Associated accounts */ - accounts: Array; - created: Scalars['DateTime']; - /** - * Best-effort human readable name for this actor for use in user interfaces during action attribution. - * For example, when displaying a sentence indicating that actor X created a build or published an update. - */ - displayName: Scalars['String']; - /** Experiments associated with this actor */ - experiments: Array; - /** - * Server feature gate values for this actor, optionally filtering by desired gates. - * Only resolves for the viewer. - */ - featureGates: Scalars['JSONObject']; - firstName?: Maybe; - id: Scalars['ID']; - isExpoAdmin: Scalars['Boolean']; - lastDeletionAttemptTime?: Maybe; -}; - - -/** A regular user, SSO user, or robot that can authenticate with Expo services and be a member of accounts. */ -export type ActorFeatureGatesArgs = { - filter?: InputMaybe>; -}; - -export type ActorExperiment = { - __typename?: 'ActorExperiment'; - createdAt: Scalars['DateTime']; - enabled: Scalars['Boolean']; - experiment: Experiment; - id: Scalars['ID']; - updatedAt: Scalars['DateTime']; -}; - -export type ActorExperimentMutation = { - __typename?: 'ActorExperimentMutation'; - /** Create or update the value of a User Experiment */ - createOrUpdateActorExperiment: ActorExperiment; -}; - - -export type ActorExperimentMutationCreateOrUpdateActorExperimentArgs = { - enabled: Scalars['Boolean']; - experiment: Experiment; -}; - -export type ActorQuery = { - __typename?: 'ActorQuery'; - /** - * Query an Actor by ID - * @deprecated Public actor queries are no longer supported - */ - byId: Actor; -}; - - -export type ActorQueryByIdArgs = { - id: Scalars['ID']; -}; - -export type AddUserInput = { - audience?: InputMaybe; - email: Scalars['String']; - tags?: InputMaybe>; -}; - -export type AddUserPayload = { - __typename?: 'AddUserPayload'; - email_address?: Maybe; - id?: Maybe; - list_id?: Maybe; - status?: Maybe; - tags?: Maybe>; - timestamp_signup?: Maybe; -}; - -export type AddonDetails = { - __typename?: 'AddonDetails'; - id: Scalars['ID']; - name: Scalars['String']; - nextInvoice?: Maybe; - planId: Scalars['String']; - quantity?: Maybe; - willCancel?: Maybe; -}; - -export type Address = { - __typename?: 'Address'; - city?: Maybe; - country?: Maybe; - line1?: Maybe; - state?: Maybe; - zip?: Maybe; -}; - -export type AndroidAppBuildCredentials = { - __typename?: 'AndroidAppBuildCredentials'; - androidKeystore?: Maybe; - id: Scalars['ID']; - isDefault: Scalars['Boolean']; - isLegacy: Scalars['Boolean']; - name: Scalars['String']; -}; - -/** @isDefault: if set, these build credentials will become the default for the Android app. All other build credentials will have their default status set to false. */ -export type AndroidAppBuildCredentialsInput = { - isDefault: Scalars['Boolean']; - keystoreId: Scalars['ID']; - name: Scalars['String']; -}; - -export type AndroidAppBuildCredentialsMutation = { - __typename?: 'AndroidAppBuildCredentialsMutation'; - /** Create a set of build credentials for an Android app */ - createAndroidAppBuildCredentials: AndroidAppBuildCredentials; - /** delete a set of build credentials for an Android app */ - deleteAndroidAppBuildCredentials: DeleteAndroidAppBuildCredentialsResult; - /** Set the build credentials to be the default for the Android app */ - setDefault: AndroidAppBuildCredentials; - /** Set the keystore to be used for an Android app */ - setKeystore: AndroidAppBuildCredentials; - /** Set the name of a set of build credentials to be used for an Android app */ - setName: AndroidAppBuildCredentials; -}; - - -export type AndroidAppBuildCredentialsMutationCreateAndroidAppBuildCredentialsArgs = { - androidAppBuildCredentialsInput: AndroidAppBuildCredentialsInput; - androidAppCredentialsId: Scalars['ID']; -}; - - -export type AndroidAppBuildCredentialsMutationDeleteAndroidAppBuildCredentialsArgs = { - id: Scalars['ID']; -}; - - -export type AndroidAppBuildCredentialsMutationSetDefaultArgs = { - id: Scalars['ID']; - isDefault: Scalars['Boolean']; -}; - - -export type AndroidAppBuildCredentialsMutationSetKeystoreArgs = { - id: Scalars['ID']; - keystoreId: Scalars['ID']; -}; - - -export type AndroidAppBuildCredentialsMutationSetNameArgs = { - id: Scalars['ID']; - name: Scalars['String']; -}; - -export type AndroidAppCredentials = { - __typename?: 'AndroidAppCredentials'; - /** @deprecated use androidAppBuildCredentialsList instead */ - androidAppBuildCredentialsArray: Array; - androidAppBuildCredentialsList: Array; - androidFcm?: Maybe; - app: App; - applicationIdentifier?: Maybe; - googleServiceAccountKeyForFcmV1?: Maybe; - googleServiceAccountKeyForSubmissions?: Maybe; - id: Scalars['ID']; - isLegacy: Scalars['Boolean']; -}; - -export type AndroidAppCredentialsFilter = { - applicationIdentifier?: InputMaybe; - legacyOnly?: InputMaybe; -}; - -export type AndroidAppCredentialsInput = { - fcmId?: InputMaybe; - googleServiceAccountKeyForFcmV1Id?: InputMaybe; - googleServiceAccountKeyForSubmissionsId?: InputMaybe; -}; - -export type AndroidAppCredentialsMutation = { - __typename?: 'AndroidAppCredentialsMutation'; - /** Create a set of credentials for an Android app */ - createAndroidAppCredentials: AndroidAppCredentials; - /** - * Create a GoogleServiceAccountKeyEntity to store credential and - * connect it with an edge from AndroidAppCredential - */ - createFcmV1Credential: AndroidAppCredentials; - /** Delete a set of credentials for an Android app */ - deleteAndroidAppCredentials: DeleteAndroidAppCredentialsResult; - /** Set the FCM push key to be used in an Android app */ - setFcm: AndroidAppCredentials; - /** Set the Google Service Account Key to be used for Firebase Cloud Messaging V1 */ - setGoogleServiceAccountKeyForFcmV1: AndroidAppCredentials; - /** Set the Google Service Account Key to be used for submitting an Android app */ - setGoogleServiceAccountKeyForSubmissions: AndroidAppCredentials; -}; - - -export type AndroidAppCredentialsMutationCreateAndroidAppCredentialsArgs = { - androidAppCredentialsInput: AndroidAppCredentialsInput; - appId: Scalars['ID']; - applicationIdentifier: Scalars['String']; -}; - - -export type AndroidAppCredentialsMutationCreateFcmV1CredentialArgs = { - accountId: Scalars['ID']; - androidAppCredentialsId: Scalars['String']; - credential: Scalars['String']; -}; - - -export type AndroidAppCredentialsMutationDeleteAndroidAppCredentialsArgs = { - id: Scalars['ID']; -}; - - -export type AndroidAppCredentialsMutationSetFcmArgs = { - fcmId: Scalars['ID']; - id: Scalars['ID']; -}; - - -export type AndroidAppCredentialsMutationSetGoogleServiceAccountKeyForFcmV1Args = { - googleServiceAccountKeyId: Scalars['ID']; - id: Scalars['ID']; -}; - - -export type AndroidAppCredentialsMutationSetGoogleServiceAccountKeyForSubmissionsArgs = { - googleServiceAccountKeyId: Scalars['ID']; - id: Scalars['ID']; -}; - -export enum AndroidBuildType { - Apk = 'APK', - AppBundle = 'APP_BUNDLE', - /** @deprecated Use developmentClient option instead. */ - DevelopmentClient = 'DEVELOPMENT_CLIENT' -} - -export type AndroidBuilderEnvironmentInput = { - bun?: InputMaybe; - env?: InputMaybe; - expoCli?: InputMaybe; - image?: InputMaybe; - ndk?: InputMaybe; - node?: InputMaybe; - pnpm?: InputMaybe; - yarn?: InputMaybe; -}; - -export type AndroidFcm = { - __typename?: 'AndroidFcm'; - account: Account; - createdAt: Scalars['DateTime']; - /** - * Legacy FCM: returns the Cloud Messaging token, parses to a String - * FCM v1: returns the Service Account Key file, parses to an Object - */ - credential: Scalars['JSON']; - id: Scalars['ID']; - snippet: FcmSnippet; - updatedAt: Scalars['DateTime']; - version: AndroidFcmVersion; -}; - -export type AndroidFcmInput = { - credential: Scalars['String']; - version: AndroidFcmVersion; -}; - -export type AndroidFcmMutation = { - __typename?: 'AndroidFcmMutation'; - /** Create an FCM credential */ - createAndroidFcm: AndroidFcm; - /** Delete an FCM credential */ - deleteAndroidFcm: DeleteAndroidFcmResult; -}; - - -export type AndroidFcmMutationCreateAndroidFcmArgs = { - accountId: Scalars['ID']; - androidFcmInput: AndroidFcmInput; -}; - - -export type AndroidFcmMutationDeleteAndroidFcmArgs = { - id: Scalars['ID']; -}; - -export enum AndroidFcmVersion { - Legacy = 'LEGACY', - V1 = 'V1' -} - -export type AndroidJobBuildCredentialsInput = { - keystore: AndroidJobKeystoreInput; -}; - -export type AndroidJobInput = { - applicationArchivePath?: InputMaybe; - /** @deprecated */ - artifactPath?: InputMaybe; - buildArtifactPaths?: InputMaybe>; - buildProfile?: InputMaybe; - buildType?: InputMaybe; - builderEnvironment?: InputMaybe; - cache?: InputMaybe; - customBuildConfig?: InputMaybe; - developmentClient?: InputMaybe; - experimental?: InputMaybe; - gradleCommand?: InputMaybe; - loggerLevel?: InputMaybe; - mode?: InputMaybe; - projectArchive: ProjectArchiveSourceInput; - projectRootDirectory: Scalars['String']; - releaseChannel?: InputMaybe; - secrets?: InputMaybe; - triggeredBy?: InputMaybe; - type: BuildWorkflow; - updates?: InputMaybe; - username?: InputMaybe; - version?: InputMaybe; -}; - -export type AndroidJobKeystoreInput = { - dataBase64: Scalars['String']; - keyAlias: Scalars['String']; - keyPassword?: InputMaybe; - keystorePassword: Scalars['String']; -}; - -export type AndroidJobOverridesInput = { - applicationArchivePath?: InputMaybe; - /** @deprecated */ - artifactPath?: InputMaybe; - buildArtifactPaths?: InputMaybe>; - buildProfile?: InputMaybe; - buildType?: InputMaybe; - builderEnvironment?: InputMaybe; - cache?: InputMaybe; - customBuildConfig?: InputMaybe; - developmentClient?: InputMaybe; - experimental?: InputMaybe; - gradleCommand?: InputMaybe; - loggerLevel?: InputMaybe; - mode?: InputMaybe; - releaseChannel?: InputMaybe; - secrets?: InputMaybe; - updates?: InputMaybe; - username?: InputMaybe; - version?: InputMaybe; -}; - -export type AndroidJobSecretsInput = { - buildCredentials?: InputMaybe; - robotAccessToken?: InputMaybe; -}; - -export type AndroidJobVersionInput = { - versionCode: Scalars['String']; -}; - -export type AndroidKeystore = { - __typename?: 'AndroidKeystore'; - account: Account; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - keyAlias: Scalars['String']; - keyPassword?: Maybe; - keystore: Scalars['String']; - keystorePassword: Scalars['String']; - md5CertificateFingerprint?: Maybe; - sha1CertificateFingerprint?: Maybe; - sha256CertificateFingerprint?: Maybe; - type: AndroidKeystoreType; - updatedAt: Scalars['DateTime']; -}; - -export type AndroidKeystoreInput = { - base64EncodedKeystore: Scalars['String']; - keyAlias: Scalars['String']; - keyPassword?: InputMaybe; - keystorePassword: Scalars['String']; -}; - -export type AndroidKeystoreMutation = { - __typename?: 'AndroidKeystoreMutation'; - /** Create a Keystore */ - createAndroidKeystore?: Maybe; - /** Delete a Keystore */ - deleteAndroidKeystore: DeleteAndroidKeystoreResult; -}; - - -export type AndroidKeystoreMutationCreateAndroidKeystoreArgs = { - accountId: Scalars['ID']; - androidKeystoreInput: AndroidKeystoreInput; -}; - - -export type AndroidKeystoreMutationDeleteAndroidKeystoreArgs = { - id: Scalars['ID']; -}; - -export enum AndroidKeystoreType { - Jks = 'JKS', - Pkcs12 = 'PKCS12', - Unknown = 'UNKNOWN' -} - -export type AndroidSubmissionConfig = { - __typename?: 'AndroidSubmissionConfig'; - /** @deprecated applicationIdentifier is deprecated and will be auto-detected on submit */ - applicationIdentifier?: Maybe; - /** @deprecated archiveType is deprecated and will be null */ - archiveType?: Maybe; - releaseStatus?: Maybe; - rollout?: Maybe; - track: SubmissionAndroidTrack; -}; - -export type AndroidSubmissionConfigInput = { - applicationIdentifier?: InputMaybe; - archiveUrl?: InputMaybe; - changesNotSentForReview?: InputMaybe; - googleServiceAccountKeyId?: InputMaybe; - googleServiceAccountKeyJson?: InputMaybe; - isVerboseFastlaneEnabled?: InputMaybe; - releaseStatus?: InputMaybe; - rollout?: InputMaybe; - track: SubmissionAndroidTrack; -}; - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type App = Project & { - __typename?: 'App'; - /** @deprecated Legacy access tokens are deprecated */ - accessTokens: Array>; - /** Coalesced project activity for an app */ - activityTimelineProjectActivities: Array; - /** Android app credentials for the project */ - androidAppCredentials: Array; - /** - * ios.appStoreUrl field from most recent classic update manifest - * @deprecated Classic updates have been deprecated. - */ - appStoreUrl?: Maybe; - assetLimitPerUpdateGroup: Scalars['Int']; - branchesPaginated: AppBranchesConnection; - /** (EAS Build) Builds associated with this app */ - builds: Array; - buildsPaginated: AppBuildsConnection; - /** - * Classic update release channel names that have at least one build - * @deprecated Classic updates have been deprecated. - */ - buildsReleaseChannels: Array; - channelsPaginated: AppChannelsConnection; - deployment?: Maybe; - /** Deployments associated with this app */ - deployments: DeploymentsConnection; - /** @deprecated Classic updates have been deprecated. */ - description: Scalars['String']; - devDomainName?: Maybe; - /** Environment secrets for an app */ - environmentSecrets: Array; - /** Environment variables for an app */ - environmentVariables: Array; - /** Environment variables for an app with decrypted secret values */ - environmentVariablesIncludingSensitive: Array; - fullName: Scalars['String']; - githubBuildTriggers: Array; - githubJobRunTriggers: Array; - githubRepository?: Maybe; - githubRepositorySettings?: Maybe; - /** - * githubUrl field from most recent classic update manifest - * @deprecated Classic updates have been deprecated. - */ - githubUrl?: Maybe; - /** - * Info about the icon specified in the most recent classic update manifest - * @deprecated Classic updates have been deprecated. - */ - icon?: Maybe; - /** @deprecated No longer supported */ - iconUrl?: Maybe; - id: Scalars['ID']; - /** App query field for querying EAS Insights about this app */ - insights: AppInsights; - internalDistributionBuildPrivacy: AppInternalDistributionBuildPrivacy; - /** iOS app credentials for the project */ - iosAppCredentials: Array; - /** @deprecated Use lastDeletionAttemptTime !== null instead */ - isDeleting: Scalars['Boolean']; - /** - * Whether the latest classic update publish is using a deprecated SDK version - * @deprecated Classic updates have been deprecated. - */ - isDeprecated: Scalars['Boolean']; - /** @deprecated 'likes' have been deprecated. */ - isLikedByMe: Scalars['Boolean']; - lastDeletionAttemptTime?: Maybe; - /** @deprecated No longer supported */ - lastPublishedTime: Scalars['DateTime']; - /** Time of the last user activity (update, branch, submission). */ - latestActivity: Scalars['DateTime']; - latestAppVersionByPlatformAndApplicationIdentifier?: Maybe; - /** @deprecated Classic updates have been deprecated. */ - latestReleaseForReleaseChannel?: Maybe; - /** - * ID of latest classic update release - * @deprecated Classic updates have been deprecated. - */ - latestReleaseId: Scalars['ID']; - /** @deprecated 'likes' have been deprecated. */ - likeCount: Scalars['Int']; - /** @deprecated 'likes' have been deprecated. */ - likedBy: Array>; - name: Scalars['String']; - ownerAccount: Account; - /** @deprecated No longer supported */ - packageName: Scalars['String']; - /** @deprecated No longer supported */ - packageUsername: Scalars['String']; - /** - * android.playStoreUrl field from most recent classic update manifest - * @deprecated Classic updates have been deprecated. - */ - playStoreUrl?: Maybe; - /** @deprecated Use 'privacySetting' instead. */ - privacy: Scalars['String']; - privacySetting: AppPrivacy; - /** - * Whether there have been any classic update publishes - * @deprecated Classic updates have been deprecated. - */ - published: Scalars['Boolean']; - /** App query field for querying details about an app's push notifications */ - pushNotifications: AppPushNotifications; - pushSecurityEnabled: Scalars['Boolean']; - /** - * Classic update release channel names (to be removed) - * @deprecated Classic updates have been deprecated. - */ - releaseChannels: Array; - /** @deprecated Legacy access tokens are deprecated */ - requiresAccessTokenForPushSecurity: Scalars['Boolean']; - resourceClassExperiment?: Maybe; - /** Runtimes associated with this app */ - runtimes: RuntimesConnection; - scopeKey: Scalars['String']; - /** - * SDK version of the latest classic update publish, 0.0.0 otherwise - * @deprecated Classic updates have been deprecated. - */ - sdkVersion: Scalars['String']; - slug: Scalars['String']; - /** EAS Submissions associated with this app */ - submissions: Array; - submissionsPaginated: AppSubmissionsConnection; - suggestedDevDomainName: Scalars['String']; - /** Coalesced project activity for an app using pagination */ - timelineActivity: TimelineActivityConnection; - /** @deprecated 'likes' have been deprecated. */ - trendScore: Scalars['Float']; - /** get an EAS branch owned by the app by name */ - updateBranchByName?: Maybe; - /** EAS branches owned by an app */ - updateBranches: Array; - /** get an EAS channel owned by the app by name */ - updateChannelByName?: Maybe; - /** EAS channels owned by an app */ - updateChannels: Array; - /** EAS updates owned by an app grouped by update group */ - updateGroups: Array>; - /** - * Time of last classic update publish - * @deprecated Classic updates have been deprecated. - */ - updated: Scalars['DateTime']; - /** EAS updates owned by an app */ - updates: Array; - updatesPaginated: AppUpdatesConnection; - /** @deprecated Use ownerAccount.name instead */ - username: Scalars['String']; - /** @deprecated No longer supported */ - users?: Maybe>>; - /** Webhooks for an app */ - webhooks: Array; - workerCustomDomain?: Maybe; - workerDeployment?: Maybe; - workerDeploymentAlias?: Maybe; - workerDeploymentAliases: WorkerDeploymentAliasesConnection; - workerDeployments: WorkerDeploymentsConnection; - workerDeploymentsCrash: WorkerDeploymentCrashEdge; - workerDeploymentsCrashes?: Maybe; - workerDeploymentsRequest: WorkerDeploymentRequestEdge; - workerDeploymentsRequests?: Maybe; - workflowRunsPaginated: AppWorkflowRunsConnection; - workflows: Array; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppActivityTimelineProjectActivitiesArgs = { - createdBefore?: InputMaybe; - filterChannels?: InputMaybe>; - filterPlatforms?: InputMaybe>; - filterTypes?: InputMaybe>; - limit: Scalars['Int']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppAndroidAppCredentialsArgs = { - filter?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppBranchesPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppBuildsArgs = { - filter?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; - platform?: InputMaybe; - status?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppBuildsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppChannelsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppDeploymentArgs = { - channel: Scalars['String']; - runtimeVersion: Scalars['String']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppDeploymentsArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppEnvironmentSecretsArgs = { - filterNames?: InputMaybe>; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppEnvironmentVariablesArgs = { - environment?: InputMaybe; - filterNames?: InputMaybe>; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppEnvironmentVariablesIncludingSensitiveArgs = { - environment?: InputMaybe; - filterNames?: InputMaybe>; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppIosAppCredentialsArgs = { - filter?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppLatestAppVersionByPlatformAndApplicationIdentifierArgs = { - applicationIdentifier: Scalars['String']; - platform: AppPlatform; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppLatestReleaseForReleaseChannelArgs = { - platform: AppPlatform; - releaseChannel: Scalars['String']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppLikedByArgs = { - limit?: InputMaybe; - offset?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppRuntimesArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppSubmissionsArgs = { - filter: SubmissionFilter; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppSubmissionsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppTimelineActivityArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppUpdateBranchByNameArgs = { - name: Scalars['String']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppUpdateBranchesArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppUpdateChannelByNameArgs = { - name: Scalars['String']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppUpdateChannelsArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppUpdateGroupsArgs = { - filter?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppUpdatesArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppUpdatesPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWebhooksArgs = { - filter?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentArgs = { - deploymentIdentifier: Scalars['WorkerDeploymentIdentifier']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentAliasArgs = { - aliasName?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentAliasesArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentsCrashArgs = { - crashKey: Scalars['ID']; - sampleFor?: InputMaybe; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentsCrashesArgs = { - filters?: InputMaybe; - timespan: DatasetTimespan; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentsRequestArgs = { - requestKey: Scalars['ID']; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkerDeploymentsRequestsArgs = { - filters?: InputMaybe; - timespan: DatasetTimespan; -}; - - -/** Represents an Exponent App (or Experience in legacy terms) */ -export type AppWorkflowRunsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type AppBranchEdge = { - __typename?: 'AppBranchEdge'; - cursor: Scalars['String']; - node: UpdateBranch; -}; - -export type AppBranchesConnection = { - __typename?: 'AppBranchesConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AppBuildEdge = { - __typename?: 'AppBuildEdge'; - cursor: Scalars['String']; - node: BuildOrBuildJob; -}; - -export type AppBuildsConnection = { - __typename?: 'AppBuildsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AppChannelEdge = { - __typename?: 'AppChannelEdge'; - cursor: Scalars['String']; - node: UpdateChannel; -}; - -export type AppChannelsConnection = { - __typename?: 'AppChannelsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AppDataInput = { - id: Scalars['ID']; - internalDistributionBuildPrivacy?: InputMaybe; - privacy?: InputMaybe; -}; - -export type AppDevDomainName = { - __typename?: 'AppDevDomainName'; - app?: Maybe; - id: Scalars['ID']; - name: Scalars['DevDomainName']; -}; - -export type AppDevDomainNameMutation = { - __typename?: 'AppDevDomainNameMutation'; - /** Creates a DevDomainName assigning it to an app */ - assignDevDomainName: AppDevDomainName; - /** Updates a DevDomainName for a given app */ - changeDevDomainName: AppDevDomainName; -}; - - -export type AppDevDomainNameMutationAssignDevDomainNameArgs = { - appId: Scalars['ID']; - name: Scalars['DevDomainName']; -}; - - -export type AppDevDomainNameMutationChangeDevDomainNameArgs = { - appId: Scalars['ID']; - name: Scalars['DevDomainName']; -}; - -export type AppIcon = { - __typename?: 'AppIcon'; - /** @deprecated No longer supported */ - colorPalette?: Maybe; - originalUrl: Scalars['String']; - primaryColor?: Maybe; - url: Scalars['String']; -}; - -export type AppInfoInput = { - displayName?: InputMaybe; -}; - -export type AppInput = { - accountId: Scalars['ID']; - appInfo?: InputMaybe; - privacy: AppPrivacy; - projectName: Scalars['String']; -}; - -export type AppInsights = { - __typename?: 'AppInsights'; - hasEventsFromExpoInsightsClientModule: Scalars['Boolean']; - totalUniqueUsers?: Maybe; - uniqueUsersByAppVersionOverTime: UniqueUsersOverTimeData; - uniqueUsersByPlatformOverTime: UniqueUsersOverTimeData; -}; - - -export type AppInsightsTotalUniqueUsersArgs = { - timespan: InsightsTimespan; -}; - - -export type AppInsightsUniqueUsersByAppVersionOverTimeArgs = { - timespan: InsightsTimespan; -}; - - -export type AppInsightsUniqueUsersByPlatformOverTimeArgs = { - timespan: InsightsTimespan; -}; - -export enum AppInternalDistributionBuildPrivacy { - Private = 'PRIVATE', - Public = 'PUBLIC' -} - -export type AppMutation = { - __typename?: 'AppMutation'; - /** Create an app */ - createApp: App; - /** Create an app and GitHub repository if user desire to */ - createAppAndGithubRepository: CreateAppAndGithubRepositoryResponse; - /** @deprecated No longer supported */ - grantAccess?: Maybe; - /** Delete an App. Returns the ID of the background job receipt. Use BackgroundJobReceiptQuery to get the status of the job. */ - scheduleAppDeletion: BackgroundJobReceipt; - /** Set display info for app */ - setAppInfo: App; - /** Require api token to send push notifs for experience */ - setPushSecurityEnabled: App; - /** Set resource class experiment for app */ - setResourceClassExperiment: App; -}; - - -export type AppMutationCreateAppArgs = { - appInput: AppInput; -}; - - -export type AppMutationCreateAppAndGithubRepositoryArgs = { - appInput: AppWithGithubRepositoryInput; -}; - - -export type AppMutationGrantAccessArgs = { - accessLevel?: InputMaybe; - toUser: Scalars['ID']; -}; - - -export type AppMutationScheduleAppDeletionArgs = { - appId: Scalars['ID']; -}; - - -export type AppMutationSetAppInfoArgs = { - appId: Scalars['ID']; - appInfo: AppInfoInput; -}; - - -export type AppMutationSetPushSecurityEnabledArgs = { - appId: Scalars['ID']; - pushSecurityEnabled: Scalars['Boolean']; -}; - - -export type AppMutationSetResourceClassExperimentArgs = { - appId: Scalars['ID']; - resourceClassExperiment?: InputMaybe; -}; - -export type AppNotificationSubscriptionInput = { - appId: Scalars['ID']; - event: NotificationEvent; - type: NotificationType; - userId: Scalars['ID']; -}; - -export enum AppPlatform { - Android = 'ANDROID', - Ios = 'IOS' -} - -export enum AppPrivacy { - Hidden = 'HIDDEN', - Public = 'PUBLIC', - Unlisted = 'UNLISTED' -} - -export type AppPushNotifications = { - __typename?: 'AppPushNotifications'; - id: Scalars['ID']; - insights: AppPushNotificationsInsights; -}; - -export type AppPushNotificationsInsights = { - __typename?: 'AppPushNotificationsInsights'; - id: Scalars['ID']; - notificationsSentOverTime: NotificationsSentOverTimeData; - successFailureOverTime: NotificationsSentOverTimeData; - totalNotificationsSent: Scalars['Int']; -}; - - -export type AppPushNotificationsInsightsNotificationsSentOverTimeArgs = { - timespan: InsightsTimespan; -}; - - -export type AppPushNotificationsInsightsSuccessFailureOverTimeArgs = { - timespan: InsightsTimespan; -}; - - -export type AppPushNotificationsInsightsTotalNotificationsSentArgs = { - filters?: InputMaybe>; - timespan: InsightsTimespan; -}; - -export type AppQuery = { - __typename?: 'AppQuery'; - /** - * Public apps in the app directory - * @deprecated App directory no longer supported - */ - all: Array; - /** Look up app by dev domain name, if one has been created */ - byDevDomainName: App; - byFullName: App; - /** Look up app by app id */ - byId: App; -}; - - -export type AppQueryAllArgs = { - filter: AppsFilter; - limit?: InputMaybe; - offset?: InputMaybe; - sort: AppSort; -}; - - -export type AppQueryByDevDomainNameArgs = { - name: Scalars['DevDomainName']; -}; - - -export type AppQueryByFullNameArgs = { - fullName: Scalars['String']; -}; - - -export type AppQueryByIdArgs = { - appId: Scalars['String']; -}; - -export type AppRelease = { - __typename?: 'AppRelease'; - hash: Scalars['String']; - id: Scalars['ID']; - manifest: Scalars['JSON']; - publishedTime: Scalars['DateTime']; - publishingUsername: Scalars['String']; - runtimeVersion?: Maybe; - s3Key: Scalars['String']; - s3Url: Scalars['String']; - sdkVersion: Scalars['String']; - version: Scalars['String']; -}; - -export enum AppSort { - /** Sort by recently published */ - RecentlyPublished = 'RECENTLY_PUBLISHED', - /** Sort by highest trendScore */ - Viewed = 'VIEWED' -} - -export type AppStoreConnectApiKey = { - __typename?: 'AppStoreConnectApiKey'; - account: Account; - appleTeam?: Maybe; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - issuerIdentifier: Scalars['String']; - keyIdentifier: Scalars['String']; - keyP8: Scalars['String']; - name?: Maybe; - roles?: Maybe>; - updatedAt: Scalars['DateTime']; -}; - -export type AppStoreConnectApiKeyInput = { - appleTeamId?: InputMaybe; - issuerIdentifier: Scalars['String']; - keyIdentifier: Scalars['String']; - keyP8: Scalars['String']; - name?: InputMaybe; - roles?: InputMaybe>; -}; - -export type AppStoreConnectApiKeyMutation = { - __typename?: 'AppStoreConnectApiKeyMutation'; - /** Create an App Store Connect Api Key for an Apple Team */ - createAppStoreConnectApiKey: AppStoreConnectApiKey; - /** Delete an App Store Connect Api Key */ - deleteAppStoreConnectApiKey: DeleteAppStoreConnectApiKeyResult; - /** Update an App Store Connect Api Key for an Apple Team */ - updateAppStoreConnectApiKey: AppStoreConnectApiKey; -}; - - -export type AppStoreConnectApiKeyMutationCreateAppStoreConnectApiKeyArgs = { - accountId: Scalars['ID']; - appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput; -}; - - -export type AppStoreConnectApiKeyMutationDeleteAppStoreConnectApiKeyArgs = { - id: Scalars['ID']; -}; - - -export type AppStoreConnectApiKeyMutationUpdateAppStoreConnectApiKeyArgs = { - appStoreConnectApiKeyUpdateInput: AppStoreConnectApiKeyUpdateInput; - id: Scalars['ID']; -}; - -export type AppStoreConnectApiKeyQuery = { - __typename?: 'AppStoreConnectApiKeyQuery'; - byId: AppStoreConnectApiKey; -}; - - -export type AppStoreConnectApiKeyQueryByIdArgs = { - id: Scalars['ID']; -}; - -export type AppStoreConnectApiKeyUpdateInput = { - appleTeamId?: InputMaybe; -}; - -export enum AppStoreConnectUserRole { - AccessToReports = 'ACCESS_TO_REPORTS', - AccountHolder = 'ACCOUNT_HOLDER', - Admin = 'ADMIN', - AppManager = 'APP_MANAGER', - CloudManagedAppDistribution = 'CLOUD_MANAGED_APP_DISTRIBUTION', - CloudManagedDeveloperId = 'CLOUD_MANAGED_DEVELOPER_ID', - CreateApps = 'CREATE_APPS', - CustomerSupport = 'CUSTOMER_SUPPORT', - Developer = 'DEVELOPER', - Finance = 'FINANCE', - ImageManager = 'IMAGE_MANAGER', - Marketing = 'MARKETING', - ReadOnly = 'READ_ONLY', - Sales = 'SALES', - Technical = 'TECHNICAL', - Unknown = 'UNKNOWN' -} - -export type AppSubmissionEdge = { - __typename?: 'AppSubmissionEdge'; - cursor: Scalars['String']; - node: Submission; -}; - -export type AppSubmissionsConnection = { - __typename?: 'AppSubmissionsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AppUpdateEdge = { - __typename?: 'AppUpdateEdge'; - cursor: Scalars['String']; - node: Update; -}; - -export type AppUpdatesConnection = { - __typename?: 'AppUpdatesConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -/** Represents Play Store/App Store version of an application */ -export type AppVersion = { - __typename?: 'AppVersion'; - /** - * Store identifier for an application - * - Android - applicationId - * - iOS - bundle identifier - */ - applicationIdentifier: Scalars['String']; - /** - * Value that identifies build in a store (it's visible to developers, but not to end users) - * - Android - versionCode in build.gradle ("android.versionCode" field in app.json) - * - iOS - CFBundleVersion in Info.plist ("ios.buildNumber" field in app.json) - */ - buildVersion: Scalars['String']; - id: Scalars['ID']; - platform: AppPlatform; - runtimeVersion?: Maybe; - /** - * User-facing version in a store - * - Android - versionName in build.gradle ("version" field in app.json) - * - iOS - CFBundleShortVersionString in Info.plist ("version" field in app.json) - */ - storeVersion: Scalars['String']; -}; - -export type AppVersionInput = { - appId: Scalars['ID']; - applicationIdentifier: Scalars['String']; - buildVersion: Scalars['String']; - platform: AppPlatform; - runtimeVersion?: InputMaybe; - storeVersion: Scalars['String']; -}; - -export type AppVersionMutation = { - __typename?: 'AppVersionMutation'; - /** Create an app version */ - createAppVersion: AppVersion; -}; - - -export type AppVersionMutationCreateAppVersionArgs = { - appVersionInput: AppVersionInput; -}; - -export type AppWithGithubRepositoryInput = { - accountId: Scalars['ID']; - appInfo?: InputMaybe; - installationIdentifier?: InputMaybe; - privacy: AppPrivacy; - projectName: Scalars['String']; -}; - -export type AppWorkflowRunEdge = { - __typename?: 'AppWorkflowRunEdge'; - cursor: Scalars['String']; - node: WorkflowRun; -}; - -export type AppWorkflowRunsConnection = { - __typename?: 'AppWorkflowRunsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AppleAppIdentifier = { - __typename?: 'AppleAppIdentifier'; - account: Account; - appleTeam?: Maybe; - bundleIdentifier: Scalars['String']; - id: Scalars['ID']; - parentAppleAppIdentifier?: Maybe; -}; - -export type AppleAppIdentifierInput = { - appleTeamId?: InputMaybe; - bundleIdentifier: Scalars['String']; - parentAppleAppId?: InputMaybe; -}; - -export type AppleAppIdentifierMutation = { - __typename?: 'AppleAppIdentifierMutation'; - /** Create an Identifier for an iOS App */ - createAppleAppIdentifier: AppleAppIdentifier; -}; - - -export type AppleAppIdentifierMutationCreateAppleAppIdentifierArgs = { - accountId: Scalars['ID']; - appleAppIdentifierInput: AppleAppIdentifierInput; -}; - -export type AppleDevice = { - __typename?: 'AppleDevice'; - account: Account; - appleTeam: AppleTeam; - createdAt: Scalars['DateTime']; - deviceClass?: Maybe; - enabled?: Maybe; - id: Scalars['ID']; - identifier: Scalars['String']; - model?: Maybe; - name?: Maybe; - softwareVersion?: Maybe; -}; - -export enum AppleDeviceClass { - Ipad = 'IPAD', - Iphone = 'IPHONE', - Mac = 'MAC', - Unknown = 'UNKNOWN' -} - -export type AppleDeviceFilterInput = { - appleTeamIdentifier?: InputMaybe; - class?: InputMaybe; - identifier?: InputMaybe; -}; - -export type AppleDeviceInput = { - appleTeamId: Scalars['ID']; - deviceClass?: InputMaybe; - enabled?: InputMaybe; - identifier: Scalars['String']; - model?: InputMaybe; - name?: InputMaybe; - softwareVersion?: InputMaybe; -}; - -export type AppleDeviceMutation = { - __typename?: 'AppleDeviceMutation'; - /** Create an Apple Device */ - createAppleDevice: AppleDevice; - /** Delete an Apple Device */ - deleteAppleDevice: DeleteAppleDeviceResult; - /** Update an Apple Device */ - updateAppleDevice: AppleDevice; -}; - - -export type AppleDeviceMutationCreateAppleDeviceArgs = { - accountId: Scalars['ID']; - appleDeviceInput: AppleDeviceInput; -}; - - -export type AppleDeviceMutationDeleteAppleDeviceArgs = { - id: Scalars['ID']; -}; - - -export type AppleDeviceMutationUpdateAppleDeviceArgs = { - appleDeviceUpdateInput: AppleDeviceUpdateInput; - id: Scalars['ID']; -}; - -export type AppleDeviceRegistrationRequest = { - __typename?: 'AppleDeviceRegistrationRequest'; - account: Account; - appleTeam: AppleTeam; - id: Scalars['ID']; -}; - -export type AppleDeviceRegistrationRequestMutation = { - __typename?: 'AppleDeviceRegistrationRequestMutation'; - /** Create an Apple Device registration request */ - createAppleDeviceRegistrationRequest: AppleDeviceRegistrationRequest; -}; - - -export type AppleDeviceRegistrationRequestMutationCreateAppleDeviceRegistrationRequestArgs = { - accountId: Scalars['ID']; - appleTeamId: Scalars['ID']; -}; - -export type AppleDeviceRegistrationRequestQuery = { - __typename?: 'AppleDeviceRegistrationRequestQuery'; - byId: AppleDeviceRegistrationRequest; -}; - - -export type AppleDeviceRegistrationRequestQueryByIdArgs = { - id: Scalars['ID']; -}; - -export type AppleDeviceUpdateInput = { - name?: InputMaybe; -}; - -export type AppleDistributionCertificate = { - __typename?: 'AppleDistributionCertificate'; - account: Account; - appleTeam?: Maybe; - certificateP12?: Maybe; - certificatePassword?: Maybe; - certificatePrivateSigningKey?: Maybe; - createdAt: Scalars['DateTime']; - developerPortalIdentifier?: Maybe; - id: Scalars['ID']; - iosAppBuildCredentialsList: Array; - serialNumber: Scalars['String']; - updatedAt: Scalars['DateTime']; - validityNotAfter: Scalars['DateTime']; - validityNotBefore: Scalars['DateTime']; -}; - -export type AppleDistributionCertificateInput = { - appleTeamId?: InputMaybe; - certP12: Scalars['String']; - certPassword: Scalars['String']; - certPrivateSigningKey?: InputMaybe; - developerPortalIdentifier?: InputMaybe; -}; - -export type AppleDistributionCertificateMutation = { - __typename?: 'AppleDistributionCertificateMutation'; - /** Create a Distribution Certificate */ - createAppleDistributionCertificate?: Maybe; - /** Delete a Distribution Certificate */ - deleteAppleDistributionCertificate: DeleteAppleDistributionCertificateResult; -}; - - -export type AppleDistributionCertificateMutationCreateAppleDistributionCertificateArgs = { - accountId: Scalars['ID']; - appleDistributionCertificateInput: AppleDistributionCertificateInput; -}; - - -export type AppleDistributionCertificateMutationDeleteAppleDistributionCertificateArgs = { - id: Scalars['ID']; -}; - -export type AppleProvisioningProfile = { - __typename?: 'AppleProvisioningProfile'; - account: Account; - appleAppIdentifier: AppleAppIdentifier; - appleDevices: Array; - appleTeam?: Maybe; - appleUUID: Scalars['String']; - createdAt: Scalars['DateTime']; - developerPortalIdentifier?: Maybe; - expiration: Scalars['DateTime']; - id: Scalars['ID']; - provisioningProfile?: Maybe; - status: Scalars['String']; - updatedAt: Scalars['DateTime']; -}; - -export type AppleProvisioningProfileInput = { - appleProvisioningProfile: Scalars['String']; - developerPortalIdentifier?: InputMaybe; -}; - -export type AppleProvisioningProfileMutation = { - __typename?: 'AppleProvisioningProfileMutation'; - /** Create a Provisioning Profile */ - createAppleProvisioningProfile: AppleProvisioningProfile; - /** Delete a Provisioning Profile */ - deleteAppleProvisioningProfile: DeleteAppleProvisioningProfileResult; - /** Delete Provisioning Profiles */ - deleteAppleProvisioningProfiles: Array; - /** Update a Provisioning Profile */ - updateAppleProvisioningProfile: AppleProvisioningProfile; -}; - - -export type AppleProvisioningProfileMutationCreateAppleProvisioningProfileArgs = { - accountId: Scalars['ID']; - appleAppIdentifierId: Scalars['ID']; - appleProvisioningProfileInput: AppleProvisioningProfileInput; -}; - - -export type AppleProvisioningProfileMutationDeleteAppleProvisioningProfileArgs = { - id: Scalars['ID']; -}; - - -export type AppleProvisioningProfileMutationDeleteAppleProvisioningProfilesArgs = { - ids: Array; -}; - - -export type AppleProvisioningProfileMutationUpdateAppleProvisioningProfileArgs = { - appleProvisioningProfileInput: AppleProvisioningProfileInput; - id: Scalars['ID']; -}; - -export type ApplePushKey = { - __typename?: 'ApplePushKey'; - account: Account; - appleTeam?: Maybe; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - iosAppCredentialsList: Array; - keyIdentifier: Scalars['String']; - keyP8: Scalars['String']; - updatedAt: Scalars['DateTime']; -}; - -export type ApplePushKeyInput = { - appleTeamId: Scalars['ID']; - keyIdentifier: Scalars['String']; - keyP8: Scalars['String']; -}; - -export type ApplePushKeyMutation = { - __typename?: 'ApplePushKeyMutation'; - /** Create an Apple Push Notification key */ - createApplePushKey: ApplePushKey; - /** Delete an Apple Push Notification key */ - deleteApplePushKey: DeleteApplePushKeyResult; -}; - - -export type ApplePushKeyMutationCreateApplePushKeyArgs = { - accountId: Scalars['ID']; - applePushKeyInput: ApplePushKeyInput; -}; - - -export type ApplePushKeyMutationDeleteApplePushKeyArgs = { - id: Scalars['ID']; -}; - -export type AppleTeam = { - __typename?: 'AppleTeam'; - account: Account; - appleAppIdentifiers: Array; - appleDevices: Array; - appleDistributionCertificates: Array; - appleProvisioningProfiles: Array; - applePushKeys: Array; - appleTeamIdentifier: Scalars['String']; - appleTeamName?: Maybe; - appleTeamType?: Maybe; - id: Scalars['ID']; -}; - - -export type AppleTeamAppleAppIdentifiersArgs = { - bundleIdentifier?: InputMaybe; -}; - - -export type AppleTeamAppleDevicesArgs = { - limit?: InputMaybe; - offset?: InputMaybe; -}; - - -export type AppleTeamAppleProvisioningProfilesArgs = { - appleAppIdentifierId?: InputMaybe; -}; - -export type AppleTeamFilterInput = { - appleTeamIdentifier?: InputMaybe; -}; - -export type AppleTeamInput = { - appleTeamIdentifier: Scalars['String']; - appleTeamName?: InputMaybe; - appleTeamType?: InputMaybe; -}; - -export type AppleTeamMutation = { - __typename?: 'AppleTeamMutation'; - /** Create an Apple Team */ - createAppleTeam: AppleTeam; - /** Update an Apple Team */ - updateAppleTeam: AppleTeam; -}; - - -export type AppleTeamMutationCreateAppleTeamArgs = { - accountId: Scalars['ID']; - appleTeamInput: AppleTeamInput; -}; - - -export type AppleTeamMutationUpdateAppleTeamArgs = { - appleTeamUpdateInput: AppleTeamUpdateInput; - id: Scalars['ID']; -}; - -export type AppleTeamQuery = { - __typename?: 'AppleTeamQuery'; - byAppleTeamIdentifier?: Maybe; -}; - - -export type AppleTeamQueryByAppleTeamIdentifierArgs = { - accountId: Scalars['ID']; - identifier: Scalars['String']; -}; - -export enum AppleTeamType { - CompanyOrOrganization = 'COMPANY_OR_ORGANIZATION', - Individual = 'INDIVIDUAL', - InHouse = 'IN_HOUSE' -} - -export type AppleTeamUpdateInput = { - appleTeamName?: InputMaybe; - appleTeamType?: InputMaybe; -}; - -export enum AppsFilter { - /** Featured Projects */ - Featured = 'FEATURED', - /** New Projects */ - New = 'NEW' -} - -export type AscApiKeyInput = { - issuerIdentifier: Scalars['String']; - keyIdentifier: Scalars['String']; - keyP8: Scalars['String']; -}; - -export type AssetMetadataResult = { - __typename?: 'AssetMetadataResult'; - status: AssetMetadataStatus; - storageKey: Scalars['String']; -}; - -export enum AssetMetadataStatus { - DoesNotExist = 'DOES_NOT_EXIST', - Exists = 'EXISTS' -} - -export type AssetMutation = { - __typename?: 'AssetMutation'; - /** - * Returns an array of specifications for upload. Each URL is valid for an hour. - * The content type of the asset you wish to upload must be specified. - */ - getSignedAssetUploadSpecifications: GetSignedAssetUploadSpecificationsResult; -}; - - -export type AssetMutationGetSignedAssetUploadSpecificationsArgs = { - assetContentTypes: Array>; -}; - -/** Check to see if assets with given storageKeys exist */ -export type AssetQuery = { - __typename?: 'AssetQuery'; - metadata: Array; -}; - - -/** Check to see if assets with given storageKeys exist */ -export type AssetQueryMetadataArgs = { - storageKeys: Array; -}; - -export type AuditLog = { - __typename?: 'AuditLog'; - account?: Maybe; - actor?: Maybe; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - metadata?: Maybe; - targetEntityId: Scalars['ID']; - targetEntityMutationType: TargetEntityMutationType; - targetEntityTypeName: EntityTypeName; - targetEntityTypePublicName: Scalars['String']; - websiteMessage: Scalars['String']; -}; - -export type AuditLogConnection = { - __typename?: 'AuditLogConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type AuditLogEdge = { - __typename?: 'AuditLogEdge'; - cursor: Scalars['String']; - node: AuditLog; -}; - -export type AuditLogExportInput = { - accountId: Scalars['ID']; - createdAfter: Scalars['String']; - createdBefore: Scalars['String']; - format: AuditLogsExportFormat; - targetEntityMutationType?: InputMaybe>; - targetEntityTypeName?: InputMaybe>; -}; - -export type AuditLogFilterInput = { - entityTypes?: InputMaybe>; - mutationTypes?: InputMaybe>; -}; - -export type AuditLogMutation = { - __typename?: 'AuditLogMutation'; - /** Exports Audit Logs for an account. Returns the ID of the background job receipt. Use BackgroundJobReceiptQuery to get the status of the job. */ - exportAuditLogs: BackgroundJobReceipt; -}; - - -export type AuditLogMutationExportAuditLogsArgs = { - exportInput: AuditLogExportInput; -}; - -export type AuditLogQuery = { - __typename?: 'AuditLogQuery'; - /** Audit logs for account */ - byId: AuditLog; - typeNamesMap: Array; -}; - - -export type AuditLogQueryByIdArgs = { - auditLogId: Scalars['ID']; -}; - -export enum AuditLogsExportFormat { - Csv = 'CSV', - Json = 'JSON', - Jsonl = 'JSONL' -} - -export enum AuthProtocolType { - Oidc = 'OIDC' -} - -export enum AuthProviderIdentifier { - GoogleWs = 'GOOGLE_WS', - MsEntraId = 'MS_ENTRA_ID', - Okta = 'OKTA', - OneLogin = 'ONE_LOGIN', - StubIdp = 'STUB_IDP' -} - -export type BackgroundJobReceipt = { - __typename?: 'BackgroundJobReceipt'; - account: Account; - createdAt: Scalars['DateTime']; - errorCode?: Maybe; - errorMessage?: Maybe; - id: Scalars['ID']; - resultData?: Maybe; - resultId?: Maybe; - resultType: BackgroundJobResultType; - state: BackgroundJobState; - tries: Scalars['Int']; - updatedAt: Scalars['DateTime']; - willRetry: Scalars['Boolean']; -}; - -export type BackgroundJobReceiptQuery = { - __typename?: 'BackgroundJobReceiptQuery'; - /** Look up background job receipt by ID */ - byId: BackgroundJobReceipt; -}; - - -export type BackgroundJobReceiptQueryByIdArgs = { - id: Scalars['ID']; -}; - -export enum BackgroundJobResultType { - AuditLogsExport = 'AUDIT_LOGS_EXPORT', - GithubBuild = 'GITHUB_BUILD', - UserAuditLogsExport = 'USER_AUDIT_LOGS_EXPORT', - Void = 'VOID' -} - -export enum BackgroundJobState { - Failure = 'FAILURE', - InProgress = 'IN_PROGRESS', - Queued = 'QUEUED', - Success = 'SUCCESS' -} - -export type Billing = { - __typename?: 'Billing'; - /** History of invoices */ - charges?: Maybe>>; - id: Scalars['ID']; - /** @deprecated No longer used */ - payment?: Maybe; - subscription?: Maybe; -}; - -export type BillingPeriod = { - __typename?: 'BillingPeriod'; - anchor: Scalars['DateTime']; - end: Scalars['DateTime']; - id: Scalars['ID']; - start: Scalars['DateTime']; -}; - -export type BranchFilterInput = { - searchTerm?: InputMaybe; -}; - -export type BranchQuery = { - __typename?: 'BranchQuery'; - /** Query a Branch by ID */ - byId: UpdateBranch; -}; - - -export type BranchQueryByIdArgs = { - branchId: Scalars['ID']; -}; - -/** Represents an EAS Build */ -export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & { - __typename?: 'Build'; - activityTimestamp: Scalars['DateTime']; - actor?: Maybe; - app: App; - appBuildVersion?: Maybe; - appIdentifier?: Maybe; - appVersion?: Maybe; - artifacts?: Maybe; - buildMode?: Maybe; - buildProfile?: Maybe; - canRetry: Scalars['Boolean']; - cancelingActor?: Maybe; - /** @deprecated Use 'updateChannel' field instead. */ - channel?: Maybe; - childBuild?: Maybe; - completedAt?: Maybe; - createdAt: Scalars['DateTime']; - customNodeVersion?: Maybe; - customWorkflowName?: Maybe; - deployment?: Maybe; - developmentClient?: Maybe; - distribution?: Maybe; - enqueuedAt?: Maybe; - error?: Maybe; - estimatedWaitTimeLeftSeconds?: Maybe; - expirationDate?: Maybe; - gitCommitHash?: Maybe; - gitCommitMessage?: Maybe; - gitRef?: Maybe; - githubRepositoryOwnerAndName?: Maybe; - id: Scalars['ID']; - /** Queue position is 1-indexed */ - initialQueuePosition?: Maybe; - initiatingActor?: Maybe; - /** @deprecated User type is deprecated */ - initiatingUser?: Maybe; - iosEnterpriseProvisioning?: Maybe; - isForIosSimulator: Scalars['Boolean']; - isGitWorkingTreeDirty?: Maybe; - isWaived: Scalars['Boolean']; - logFiles: Array; - maxBuildTimeSeconds: Scalars['Int']; - /** Retry time starts after completedAt */ - maxRetryTimeMinutes?: Maybe; - message?: Maybe; - metrics?: Maybe; - parentBuild?: Maybe; - platform: AppPlatform; - priority: BuildPriority; - /** @deprecated Use app field instead */ - project: Project; - projectMetadataFileUrl?: Maybe; - projectRootDirectory?: Maybe; - provisioningStartedAt?: Maybe; - /** Queue position is 1-indexed */ - queuePosition?: Maybe; - reactNativeVersion?: Maybe; - releaseChannel?: Maybe; - requiredPackageManager?: Maybe; - /** - * The builder resource class requested by the developer - * @deprecated Use resourceClassDisplayName instead - */ - resourceClass: BuildResourceClass; - /** String describing the resource class used to run the build */ - resourceClassDisplayName: Scalars['String']; - retryDisabledReason?: Maybe; - runFromCI?: Maybe; - runtime?: Maybe; - /** @deprecated Use 'runtime' field instead. */ - runtimeVersion?: Maybe; - sdkVersion?: Maybe; - selectedImage?: Maybe; - status: BuildStatus; - submissions: Array; - updateChannel?: Maybe; - updatedAt: Scalars['DateTime']; - workerStartedAt?: Maybe; -}; - - -/** Represents an EAS Build */ -export type BuildCanRetryArgs = { - newMode?: InputMaybe; -}; - - -/** Represents an EAS Build */ -export type BuildRetryDisabledReasonArgs = { - newMode?: InputMaybe; -}; - -export type BuildAnnotation = { - __typename?: 'BuildAnnotation'; - authorUsername?: Maybe; - buildPhase: Scalars['String']; - exampleBuildLog?: Maybe; - id: Scalars['ID']; - internalNotes?: Maybe; - message: Scalars['String']; - regexFlags?: Maybe; - regexString: Scalars['String']; - title: Scalars['String']; -}; - -export type BuildAnnotationDataInput = { - buildPhase: Scalars['String']; - exampleBuildLog?: InputMaybe; - internalNotes?: InputMaybe; - message: Scalars['String']; - regexFlags?: InputMaybe; - regexString: Scalars['String']; - title: Scalars['String']; -}; - -export type BuildAnnotationFiltersInput = { - buildPhases: Array; -}; - -export type BuildAnnotationMutation = { - __typename?: 'BuildAnnotationMutation'; - /** Create a Build Annotation */ - createBuildAnnotation: BuildAnnotation; - /** Delete a Build Annotation */ - deleteBuildAnnotation: DeleteBuildAnnotationResult; - /** Update a Build Annotation */ - updateBuildAnnotation: BuildAnnotation; -}; - - -export type BuildAnnotationMutationCreateBuildAnnotationArgs = { - buildAnnotationData: BuildAnnotationDataInput; -}; - - -export type BuildAnnotationMutationDeleteBuildAnnotationArgs = { - buildAnnotationId: Scalars['ID']; -}; - - -export type BuildAnnotationMutationUpdateBuildAnnotationArgs = { - buildAnnotationData: BuildAnnotationDataInput; - buildAnnotationId: Scalars['ID']; -}; - -export type BuildAnnotationsQuery = { - __typename?: 'BuildAnnotationsQuery'; - /** View build annotations */ - all: Array; - /** Find a build annotation by ID */ - byId: BuildAnnotation; -}; - - -export type BuildAnnotationsQueryAllArgs = { - filters?: InputMaybe; -}; - - -export type BuildAnnotationsQueryByIdArgs = { - buildAnnotationId: Scalars['ID']; -}; - -export type BuildArtifacts = { - __typename?: 'BuildArtifacts'; - applicationArchiveUrl?: Maybe; - buildArtifactsUrl?: Maybe; - buildUrl?: Maybe; - /** @deprecated Use 'runtime.fingerprintDebugInfoUrl' instead. */ - fingerprintUrl?: Maybe; - xcodeBuildLogsUrl?: Maybe; -}; - -export type BuildCacheInput = { - clear?: InputMaybe; - disabled?: InputMaybe; - key?: InputMaybe; - paths?: InputMaybe>; -}; - -export enum BuildCredentialsSource { - Local = 'LOCAL', - Remote = 'REMOTE' -} - -export type BuildError = { - __typename?: 'BuildError'; - buildPhase?: Maybe; - docsUrl?: Maybe; - errorCode: Scalars['String']; - message: Scalars['String']; -}; - -export type BuildFilter = { - appBuildVersion?: InputMaybe; - appIdentifier?: InputMaybe; - appVersion?: InputMaybe; - buildProfile?: InputMaybe; - channel?: InputMaybe; - distribution?: InputMaybe; - gitCommitHash?: InputMaybe; - platform?: InputMaybe; - runtimeVersion?: InputMaybe; - sdkVersion?: InputMaybe; - simulator?: InputMaybe; - status?: InputMaybe; -}; - -export type BuildFilterInput = { - channel?: InputMaybe; - developmentClient?: InputMaybe; - distributions?: InputMaybe>; - platforms?: InputMaybe>; - releaseChannel?: InputMaybe; - runtimeVersion?: InputMaybe; - simulator?: InputMaybe; -}; - -export enum BuildIosEnterpriseProvisioning { - Adhoc = 'ADHOC', - Universal = 'UNIVERSAL' -} - -export type BuildLimitThresholdExceededMetadata = { - __typename?: 'BuildLimitThresholdExceededMetadata'; - account: Account; - thresholdsExceeded: Array; -}; - -export enum BuildLimitThresholdExceededMetadataType { - Ios = 'IOS', - Total = 'TOTAL' -} - -export type BuildMetadataInput = { - appBuildVersion?: InputMaybe; - appIdentifier?: InputMaybe; - appName?: InputMaybe; - appVersion?: InputMaybe; - buildProfile?: InputMaybe; - channel?: InputMaybe; - cliVersion?: InputMaybe; - credentialsSource?: InputMaybe; - customNodeVersion?: InputMaybe; - customWorkflowName?: InputMaybe; - developmentClient?: InputMaybe; - distribution?: InputMaybe; - environment?: InputMaybe; - fingerprintSource?: InputMaybe; - gitCommitHash?: InputMaybe; - gitCommitMessage?: InputMaybe; - iosEnterpriseProvisioning?: InputMaybe; - isGitWorkingTreeDirty?: InputMaybe; - message?: InputMaybe; - reactNativeVersion?: InputMaybe; - releaseChannel?: InputMaybe; - requiredPackageManager?: InputMaybe; - runFromCI?: InputMaybe; - runWithNoWaitFlag?: InputMaybe; - runtimeVersion?: InputMaybe; - sdkVersion?: InputMaybe; - selectedImage?: InputMaybe; - simulator?: InputMaybe; - trackingContext?: InputMaybe; - username?: InputMaybe; - workflow?: InputMaybe; -}; - -export type BuildMetrics = { - __typename?: 'BuildMetrics'; - buildDuration?: Maybe; - buildQueueTime?: Maybe; - buildWaitTime?: Maybe; -}; - -export enum BuildMode { - Build = 'BUILD', - Custom = 'CUSTOM', - Repack = 'REPACK', - Resign = 'RESIGN' -} - -export type BuildMutation = { - __typename?: 'BuildMutation'; - /** - * Cancel an EAS Build build - * @deprecated Use cancelBuild instead - */ - cancel: Build; - /** Cancel an EAS Build build */ - cancelBuild: Build; - /** Create an Android build */ - createAndroidBuild: CreateBuildResult; - /** Create an iOS build */ - createIosBuild: CreateBuildResult; - /** Delete an EAS Build build */ - deleteBuild: Build; - /** Retry an Android EAS Build */ - retryAndroidBuild: Build; - /** - * Retry an EAS Build build - * @deprecated Use retryAndroidBuild and retryIosBuild instead - */ - retryBuild: Build; - /** Retry an iOS EAS Build */ - retryIosBuild: Build; - /** Update metadata for EAS Build build */ - updateBuildMetadata: Build; -}; - - -export type BuildMutationCancelBuildArgs = { - buildId: Scalars['ID']; -}; - - -export type BuildMutationCreateAndroidBuildArgs = { - appId: Scalars['ID']; - buildParams?: InputMaybe; - job: AndroidJobInput; - metadata?: InputMaybe; -}; - - -export type BuildMutationCreateIosBuildArgs = { - appId: Scalars['ID']; - buildParams?: InputMaybe; - job: IosJobInput; - metadata?: InputMaybe; -}; - - -export type BuildMutationDeleteBuildArgs = { - buildId: Scalars['ID']; -}; - - -export type BuildMutationRetryAndroidBuildArgs = { - buildId: Scalars['ID']; - jobOverrides?: InputMaybe; -}; - - -export type BuildMutationRetryBuildArgs = { - buildId: Scalars['ID']; -}; - - -export type BuildMutationRetryIosBuildArgs = { - buildId: Scalars['ID']; - jobOverrides?: InputMaybe; -}; - - -export type BuildMutationUpdateBuildMetadataArgs = { - buildId: Scalars['ID']; - metadata: BuildMetadataInput; -}; - -export type BuildOrBuildJob = { - id: Scalars['ID']; -}; - -export type BuildParamsInput = { - reactNativeVersion?: InputMaybe; - resourceClass: BuildResourceClass; - sdkVersion?: InputMaybe; -}; - -export enum BuildPhase { - BuilderInfo = 'BUILDER_INFO', - CleanUpCredentials = 'CLEAN_UP_CREDENTIALS', - CompleteBuild = 'COMPLETE_BUILD', - ConfigureExpoUpdates = 'CONFIGURE_EXPO_UPDATES', - ConfigureXcodeProject = 'CONFIGURE_XCODE_PROJECT', - Custom = 'CUSTOM', - DownloadApplicationArchive = 'DOWNLOAD_APPLICATION_ARCHIVE', - EasBuildInternal = 'EAS_BUILD_INTERNAL', - FailBuild = 'FAIL_BUILD', - FixGradlew = 'FIX_GRADLEW', - InstallCustomTools = 'INSTALL_CUSTOM_TOOLS', - InstallDependencies = 'INSTALL_DEPENDENCIES', - InstallPods = 'INSTALL_PODS', - OnBuildCancelHook = 'ON_BUILD_CANCEL_HOOK', - OnBuildCompleteHook = 'ON_BUILD_COMPLETE_HOOK', - OnBuildErrorHook = 'ON_BUILD_ERROR_HOOK', - OnBuildSuccessHook = 'ON_BUILD_SUCCESS_HOOK', - ParseCustomWorkflowConfig = 'PARSE_CUSTOM_WORKFLOW_CONFIG', - PostInstallHook = 'POST_INSTALL_HOOK', - Prebuild = 'PREBUILD', - PrepareArtifacts = 'PREPARE_ARTIFACTS', - PrepareCredentials = 'PREPARE_CREDENTIALS', - PrepareProject = 'PREPARE_PROJECT', - PreInstallHook = 'PRE_INSTALL_HOOK', - PreUploadArtifactsHook = 'PRE_UPLOAD_ARTIFACTS_HOOK', - Queue = 'QUEUE', - ReadAppConfig = 'READ_APP_CONFIG', - ReadPackageJson = 'READ_PACKAGE_JSON', - RestoreCache = 'RESTORE_CACHE', - RunExpoDoctor = 'RUN_EXPO_DOCTOR', - RunFastlane = 'RUN_FASTLANE', - RunGradlew = 'RUN_GRADLEW', - SaveCache = 'SAVE_CACHE', - SetUpBuildEnvironment = 'SET_UP_BUILD_ENVIRONMENT', - SpinUpBuilder = 'SPIN_UP_BUILDER', - StartBuild = 'START_BUILD', - Unknown = 'UNKNOWN', - UploadApplicationArchive = 'UPLOAD_APPLICATION_ARCHIVE', - /** @deprecated No longer supported */ - UploadArtifacts = 'UPLOAD_ARTIFACTS', - UploadBuildArtifacts = 'UPLOAD_BUILD_ARTIFACTS' -} - -export type BuildPlanCreditThresholdExceededMetadata = { - __typename?: 'BuildPlanCreditThresholdExceededMetadata'; - account: Account; - buildCreditUsage: Scalars['Int']; - planLimit: Scalars['Int']; - threshold: Scalars['Int']; -}; - -export enum BuildPriority { - High = 'HIGH', - Normal = 'NORMAL', - NormalPlus = 'NORMAL_PLUS' -} - -/** Publicly visible data for a Build. */ -export type BuildPublicData = { - __typename?: 'BuildPublicData'; - artifacts: PublicArtifacts; - distribution?: Maybe; - id: Scalars['ID']; - isForIosSimulator: Scalars['Boolean']; - platform: AppPlatform; - project: ProjectPublicData; - status: BuildStatus; -}; - -export type BuildPublicDataQuery = { - __typename?: 'BuildPublicDataQuery'; - /** Get BuildPublicData by ID */ - byId?: Maybe; -}; - - -export type BuildPublicDataQueryByIdArgs = { - id: Scalars['ID']; -}; - -export type BuildQuery = { - __typename?: 'BuildQuery'; - /** - * Get all builds. - * By default, they are sorted from latest to oldest. - * Available only for admin users. - */ - all: Array; - /** - * Get all builds for a specific app. - * They are sorted from latest to oldest. - * @deprecated Use App.builds instead - */ - allForApp: Array>; - /** Look up EAS Build by build ID */ - byId: Build; -}; - - -export type BuildQueryAllArgs = { - limit?: InputMaybe; - offset?: InputMaybe; - order?: InputMaybe; - statuses?: InputMaybe>; -}; - - -export type BuildQueryAllForAppArgs = { - appId: Scalars['String']; - limit?: InputMaybe; - offset?: InputMaybe; - platform?: InputMaybe; - status?: InputMaybe; -}; - - -export type BuildQueryByIdArgs = { - buildId: Scalars['ID']; -}; - -export type BuildResignInput = { - applicationArchiveSource?: InputMaybe; -}; - -export enum BuildResourceClass { - AndroidDefault = 'ANDROID_DEFAULT', - AndroidLarge = 'ANDROID_LARGE', - AndroidMedium = 'ANDROID_MEDIUM', - IosDefault = 'IOS_DEFAULT', - /** @deprecated No longer available. Use IOS_M_LARGE instead. */ - IosIntelLarge = 'IOS_INTEL_LARGE', - /** @deprecated No longer available. Use IOS_M_MEDIUM instead. */ - IosIntelMedium = 'IOS_INTEL_MEDIUM', - IosLarge = 'IOS_LARGE', - /** @deprecated Use IOS_M_MEDIUM instead */ - IosM1Large = 'IOS_M1_LARGE', - /** @deprecated Use IOS_M_MEDIUM instead */ - IosM1Medium = 'IOS_M1_MEDIUM', - IosMedium = 'IOS_MEDIUM', - IosMLarge = 'IOS_M_LARGE', - IosMMedium = 'IOS_M_MEDIUM', - Legacy = 'LEGACY', - LinuxLarge = 'LINUX_LARGE', - LinuxMedium = 'LINUX_MEDIUM' -} - -export enum BuildRetryDisabledReason { - AlreadyRetried = 'ALREADY_RETRIED', - InvalidStatus = 'INVALID_STATUS', - IsGithubBuild = 'IS_GITHUB_BUILD', - NotCompletedYet = 'NOT_COMPLETED_YET', - TooMuchTimeElapsed = 'TOO_MUCH_TIME_ELAPSED' -} - -export enum BuildStatus { - Canceled = 'CANCELED', - Errored = 'ERRORED', - Finished = 'FINISHED', - InProgress = 'IN_PROGRESS', - InQueue = 'IN_QUEUE', - New = 'NEW', - PendingCancel = 'PENDING_CANCEL' -} - -export enum BuildTrigger { - EasCli = 'EAS_CLI', - GitBasedIntegration = 'GIT_BASED_INTEGRATION' -} - -export type BuildUpdatesInput = { - channel?: InputMaybe; -}; - -export enum BuildWorkflow { - Generic = 'GENERIC', - Managed = 'MANAGED', - Unknown = 'UNKNOWN' -} - -export type Card = { - __typename?: 'Card'; - brand?: Maybe; - cardHolder?: Maybe; - expMonth?: Maybe; - expYear?: Maybe; - last4?: Maybe; -}; - -export type ChannelFilterInput = { - searchTerm?: InputMaybe; -}; - -export type ChannelQuery = { - __typename?: 'ChannelQuery'; - /** Query a Channel by ID */ - byId: UpdateChannel; -}; - - -export type ChannelQueryByIdArgs = { - channelId: Scalars['ID']; -}; - -export type Charge = { - __typename?: 'Charge'; - amount: Scalars['Int']; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - invoiceId?: Maybe; - paid: Scalars['Boolean']; - receiptUrl?: Maybe; - wasRefunded: Scalars['Boolean']; -}; - -export type CodeSigningInfo = { - __typename?: 'CodeSigningInfo'; - alg: Scalars['String']; - keyid: Scalars['String']; - sig: Scalars['String']; -}; - -export type CodeSigningInfoInput = { - alg: Scalars['String']; - keyid: Scalars['String']; - sig: Scalars['String']; -}; - -export type Concurrencies = { - __typename?: 'Concurrencies'; - android: Scalars['Int']; - ios: Scalars['Int']; - total: Scalars['Int']; -}; - -export enum ContinentCode { - Af = 'AF', - An = 'AN', - As = 'AS', - Eu = 'EU', - Na = 'NA', - Oc = 'OC', - Sa = 'SA', - T1 = 'T1' -} - -export enum CrashSampleFor { - Newest = 'NEWEST', - Oldest = 'OLDEST' -} - -export type CrashesFilters = { - name?: InputMaybe>; -}; - -export type CreateAccessTokenInput = { - actorID: Scalars['ID']; - note?: InputMaybe; -}; - -export type CreateAccessTokenResponse = { - __typename?: 'CreateAccessTokenResponse'; - /** AccessToken created */ - accessToken: AccessToken; - /** Full token string to be used for authentication */ - token: Scalars['String']; -}; - -export type CreateAndroidSubmissionInput = { - appId: Scalars['ID']; - archiveSource?: InputMaybe; - archiveUrl?: InputMaybe; - config: AndroidSubmissionConfigInput; - submittedBuildId?: InputMaybe; -}; - -export type CreateAppAndGithubRepositoryResponse = { - __typename?: 'CreateAppAndGithubRepositoryResponse'; - app: App; - cloneUrl?: Maybe; -}; - -export type CreateBuildResult = { - __typename?: 'CreateBuildResult'; - build: Build; - deprecationInfo?: Maybe; -}; - -export type CreateEnvironmentSecretInput = { - name: Scalars['String']; - type?: InputMaybe; - value: Scalars['String']; -}; - -export type CreateEnvironmentVariableInput = { - environments?: InputMaybe>; - fileName?: InputMaybe; - name: Scalars['String']; - overwrite?: InputMaybe; - type?: InputMaybe; - value: Scalars['String']; - visibility: EnvironmentVariableVisibility; -}; - -export type CreateGitHubAppInstallationInput = { - accountId: Scalars['ID']; - installationIdentifier: Scalars['Int']; -}; - -export type CreateGitHubBuildTriggerInput = { - appId: Scalars['ID']; - autoSubmit: Scalars['Boolean']; - buildProfile: Scalars['String']; - environment?: InputMaybe; - executionBehavior: GitHubBuildTriggerExecutionBehavior; - isActive: Scalars['Boolean']; - platform: AppPlatform; - /** A branch or tag name, or a wildcard pattern where the code change originates from. For example, `main` or `release/*`. */ - sourcePattern: Scalars['String']; - submitProfile?: InputMaybe; - /** A branch name or a wildcard pattern that the pull request targets. For example, `main` or `release/*`. */ - targetPattern?: InputMaybe; - type: GitHubBuildTriggerType; -}; - -export type CreateGitHubJobRunTriggerInput = { - appId: Scalars['ID']; - isActive: Scalars['Boolean']; - jobType: GitHubJobRunJobType; - sourcePattern: Scalars['String']; - targetPattern?: InputMaybe; - triggerType: GitHubJobRunTriggerType; -}; - -export type CreateGitHubRepositoryInput = { - appId: Scalars['ID']; - githubAppInstallationId: Scalars['ID']; - githubRepositoryIdentifier: Scalars['Int']; - nodeIdentifier: Scalars['String']; -}; - -export type CreateGitHubRepositorySettingsInput = { - appId: Scalars['ID']; - /** The base directory is the directory to change to before starting a build. This string should be a properly formatted POSIX path starting with '/', './', or the name of the directory relative to the root of the repository. Valid examples include: '/apps/expo-app', './apps/expo-app', and 'apps/expo-app'. This is intended for monorepos or apps that live in a subdirectory of a repository. */ - baseDirectory: Scalars['String']; -}; - -export type CreateIosSubmissionInput = { - appId: Scalars['ID']; - archiveSource?: InputMaybe; - archiveUrl?: InputMaybe; - config: IosSubmissionConfigInput; - submittedBuildId?: InputMaybe; -}; - -export type CreateServerlessFunctionUploadUrlResult = { - __typename?: 'CreateServerlessFunctionUploadUrlResult'; - formDataFields: Scalars['JSONObject']; - url: Scalars['String']; -}; - -export type CreateSharedEnvironmentVariableInput = { - environments?: InputMaybe>; - fileName?: InputMaybe; - isGlobal?: InputMaybe; - name: Scalars['String']; - overwrite?: InputMaybe; - type?: InputMaybe; - value: Scalars['String']; - visibility: EnvironmentVariableVisibility; -}; - -export type CreateSubmissionResult = { - __typename?: 'CreateSubmissionResult'; - /** Created submission */ - submission: Submission; -}; - -export type CumulativeMetrics = { - __typename?: 'CumulativeMetrics'; - data: UpdatesMetricsData; - metricsAtLastTimestamp: CumulativeMetricsTotals; -}; - -export type CumulativeMetricsOverTimeData = { - __typename?: 'CumulativeMetricsOverTimeData'; - data: LineChartData; - metricsAtLastTimestamp: Array; -}; - -export type CumulativeMetricsTotals = { - __typename?: 'CumulativeMetricsTotals'; - totalFailedInstalls: Scalars['Int']; - totalInstalls: Scalars['Int']; -}; - -export type CumulativeUpdatesDataset = { - __typename?: 'CumulativeUpdatesDataset'; - cumulative: Array; - difference: Array; - id: Scalars['String']; - label: Scalars['String']; -}; - -export type CustomBuildConfigInput = { - path: Scalars['String']; -}; - -export type CustomDomainDnsRecord = { - __typename?: 'CustomDomainDNSRecord'; - dnsContent: Scalars['String']; - dnsName: Scalars['String']; - dnsType: CustomDomainDnsRecordType; - isConfigured: Scalars['Boolean']; -}; - -export enum CustomDomainDnsRecordType { - A = 'A', - Cname = 'CNAME', - Txt = 'TXT' -} - -export type CustomDomainMutation = { - __typename?: 'CustomDomainMutation'; - deleteCustomDomain: DeleteCustomDomainResult; - refreshCustomDomain: WorkerCustomDomain; - registerCustomDomain: WorkerCustomDomain; -}; - - -export type CustomDomainMutationDeleteCustomDomainArgs = { - customDomainId: Scalars['ID']; -}; - - -export type CustomDomainMutationRefreshCustomDomainArgs = { - customDomainId: Scalars['ID']; -}; - - -export type CustomDomainMutationRegisterCustomDomainArgs = { - aliasName?: InputMaybe; - appId: Scalars['ID']; - hostname: Scalars['String']; -}; - -export type CustomDomainSetup = { - __typename?: 'CustomDomainSetup'; - sslErrors?: Maybe>; - sslStatus?: Maybe; - status: CustomDomainStatus; - verificationErrors?: Maybe>; - verificationStatus?: Maybe; -}; - -export enum CustomDomainStatus { - Active = 'ACTIVE', - Error = 'ERROR', - Pending = 'PENDING', - TimedOut = 'TIMED_OUT' -} - -export type DatasetTimespan = { - end: Scalars['DateTime']; - start: Scalars['DateTime']; -}; - -export type DeleteAccessTokenResult = { - __typename?: 'DeleteAccessTokenResult'; - id: Scalars['ID']; -}; - -export type DeleteAccountResult = { - __typename?: 'DeleteAccountResult'; - id: Scalars['ID']; -}; - -export type DeleteAccountSsoConfigurationResult = { - __typename?: 'DeleteAccountSSOConfigurationResult'; - id: Scalars['ID']; -}; - -export type DeleteAliasResult = { - __typename?: 'DeleteAliasResult'; - aliasName?: Maybe; - id: Scalars['ID']; -}; - -export type DeleteAndroidAppCredentialsResult = { - __typename?: 'DeleteAndroidAppCredentialsResult'; - id: Scalars['ID']; -}; - -export type DeleteAndroidKeystoreResult = { - __typename?: 'DeleteAndroidKeystoreResult'; - id: Scalars['ID']; -}; - -export type DeleteAppleDeviceResult = { - __typename?: 'DeleteAppleDeviceResult'; - id: Scalars['ID']; -}; - -export type DeleteAppleDistributionCertificateResult = { - __typename?: 'DeleteAppleDistributionCertificateResult'; - id: Scalars['ID']; -}; - -export type DeleteAppleProvisioningProfileResult = { - __typename?: 'DeleteAppleProvisioningProfileResult'; - id: Scalars['ID']; -}; - -export type DeleteBuildAnnotationResult = { - __typename?: 'DeleteBuildAnnotationResult'; - buildAnnotationId: Scalars['ID']; -}; - -export type DeleteCustomDomainResult = { - __typename?: 'DeleteCustomDomainResult'; - appId: Scalars['ID']; - hostname: Scalars['String']; - id: Scalars['ID']; -}; - -export type DeleteDiscordUserResult = { - __typename?: 'DeleteDiscordUserResult'; - id: Scalars['ID']; -}; - -export type DeleteEnvironmentSecretResult = { - __typename?: 'DeleteEnvironmentSecretResult'; - id: Scalars['ID']; -}; - -export type DeleteEnvironmentVariableResult = { - __typename?: 'DeleteEnvironmentVariableResult'; - id: Scalars['ID']; -}; - -export type DeleteGitHubUserResult = { - __typename?: 'DeleteGitHubUserResult'; - id: Scalars['ID']; -}; - -export type DeleteGoogleServiceAccountKeyResult = { - __typename?: 'DeleteGoogleServiceAccountKeyResult'; - id: Scalars['ID']; -}; - -export type DeleteIosAppBuildCredentialsResult = { - __typename?: 'DeleteIosAppBuildCredentialsResult'; - id: Scalars['ID']; -}; - -export type DeleteIosAppCredentialsResult = { - __typename?: 'DeleteIosAppCredentialsResult'; - id: Scalars['ID']; -}; - -export type DeleteRobotResult = { - __typename?: 'DeleteRobotResult'; - id: Scalars['ID']; -}; - -export type DeleteSsoUserResult = { - __typename?: 'DeleteSSOUserResult'; - id: Scalars['ID']; -}; - -export type DeleteUpdateBranchResult = { - __typename?: 'DeleteUpdateBranchResult'; - id: Scalars['ID']; -}; - -export type DeleteUpdateChannelResult = { - __typename?: 'DeleteUpdateChannelResult'; - id: Scalars['ID']; -}; - -export type DeleteUpdateGroupResult = { - __typename?: 'DeleteUpdateGroupResult'; - group: Scalars['ID']; -}; - -export type DeleteWebhookResult = { - __typename?: 'DeleteWebhookResult'; - id: Scalars['ID']; -}; - -export type DeleteWorkerDeploymentResult = { - __typename?: 'DeleteWorkerDeploymentResult'; - deploymentIdentifier: Scalars['WorkerDeploymentIdentifier']; - id: Scalars['ID']; -}; - -export type DeployServerlessFunctionResult = { - __typename?: 'DeployServerlessFunctionResult'; - url: Scalars['String']; -}; - -/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */ -export type Deployment = { - __typename?: 'Deployment'; - buildCount: Scalars['Int']; - builds: DeploymentBuildsConnection; - channel: UpdateChannel; - id: Scalars['ID']; - /** Deployment query field */ - insights: DeploymentInsights; - /** Ordered the same way as 'updateBranches' in UpdateChannel */ - latestUpdatesPerBranch: Array; - runtime: Runtime; -}; - - -/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */ -export type DeploymentBuildCountArgs = { - statuses?: InputMaybe>; -}; - - -/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */ -export type DeploymentBuildsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */ -export type DeploymentLatestUpdatesPerBranchArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - -export type DeploymentBuildEdge = { - __typename?: 'DeploymentBuildEdge'; - cursor: Scalars['String']; - node: Build; -}; - -/** Represents the connection over the builds edge of a Deployment */ -export type DeploymentBuildsConnection = { - __typename?: 'DeploymentBuildsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type DeploymentCumulativeMetricsOverTimeData = { - __typename?: 'DeploymentCumulativeMetricsOverTimeData'; - data: LineChartData; - metricsAtLastTimestamp: Array; - mostPopularUpdates: Array; -}; - -export type DeploymentEdge = { - __typename?: 'DeploymentEdge'; - cursor: Scalars['String']; - node: Deployment; -}; - -export type DeploymentFilterInput = { - channel?: InputMaybe; - runtimeVersion?: InputMaybe; -}; - -export type DeploymentInsights = { - __typename?: 'DeploymentInsights'; - cumulativeMetricsOverTime: DeploymentCumulativeMetricsOverTimeData; - embeddedUpdateTotalUniqueUsers: Scalars['Int']; - embeddedUpdateUniqueUsersOverTime: UniqueUsersOverTimeData; - id: Scalars['ID']; - mostPopularUpdates: Array; - uniqueUsersOverTime: UniqueUsersOverTimeData; -}; - - -export type DeploymentInsightsCumulativeMetricsOverTimeArgs = { - timespan: InsightsTimespan; -}; - - -export type DeploymentInsightsEmbeddedUpdateTotalUniqueUsersArgs = { - timespan: InsightsTimespan; -}; - - -export type DeploymentInsightsEmbeddedUpdateUniqueUsersOverTimeArgs = { - timespan: InsightsTimespan; -}; - - -export type DeploymentInsightsMostPopularUpdatesArgs = { - timespan: InsightsTimespan; -}; - - -export type DeploymentInsightsUniqueUsersOverTimeArgs = { - timespan: InsightsTimespan; -}; - -export type DeploymentQuery = { - __typename?: 'DeploymentQuery'; - /** Query a Deployment by ID */ - byId: Deployment; -}; - - -export type DeploymentQueryByIdArgs = { - deploymentId: Scalars['ID']; -}; - -export type DeploymentResult = { - __typename?: 'DeploymentResult'; - data?: Maybe; - error?: Maybe; - success: Scalars['Boolean']; -}; - -export type DeploymentSignedUrlResult = { - __typename?: 'DeploymentSignedUrlResult'; - deploymentIdentifier: Scalars['ID']; - pendingWorkerDeploymentId: Scalars['ID']; - url: Scalars['String']; -}; - -/** Represents the connection over the deployments edge of an App */ -export type DeploymentsConnection = { - __typename?: 'DeploymentsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type DeploymentsMutation = { - __typename?: 'DeploymentsMutation'; - assignAlias: WorkerDeploymentAlias; - /** Create a signed deployment URL */ - createSignedDeploymentUrl: DeploymentSignedUrlResult; - deleteAlias: DeleteAliasResult; - deleteWorkerDeployment: DeleteWorkerDeploymentResult; -}; - - -export type DeploymentsMutationAssignAliasArgs = { - aliasName?: InputMaybe; - appId: Scalars['ID']; - deploymentIdentifier: Scalars['ID']; -}; - - -export type DeploymentsMutationCreateSignedDeploymentUrlArgs = { - appId: Scalars['ID']; - deploymentIdentifier?: InputMaybe; -}; - - -export type DeploymentsMutationDeleteAliasArgs = { - aliasName?: InputMaybe; - appId: Scalars['ID']; -}; - - -export type DeploymentsMutationDeleteWorkerDeploymentArgs = { - workerDeploymentId: Scalars['ID']; -}; - -export type DiscordUser = { - __typename?: 'DiscordUser'; - discordIdentifier: Scalars['String']; - id: Scalars['ID']; - metadata?: Maybe; - userActor: UserActor; -}; - -export type DiscordUserMetadata = { - __typename?: 'DiscordUserMetadata'; - discordAvatarUrl: Scalars['String']; - discordDiscriminator: Scalars['String']; - discordUsername: Scalars['String']; -}; - -export type DiscordUserMutation = { - __typename?: 'DiscordUserMutation'; - /** Delete a Discord User by ID */ - deleteDiscordUser: DeleteDiscordUserResult; -}; - - -export type DiscordUserMutationDeleteDiscordUserArgs = { - id: Scalars['ID']; -}; - -export enum DistributionType { - Internal = 'INTERNAL', - Simulator = 'SIMULATOR', - Store = 'STORE' -} - -export enum EasBuildBillingResourceClass { - Large = 'LARGE', - Medium = 'MEDIUM' -} - -export type EasBuildDeprecationInfo = { - __typename?: 'EASBuildDeprecationInfo'; - message: Scalars['String']; - type: EasBuildDeprecationInfoType; -}; - -export enum EasBuildDeprecationInfoType { - Internal = 'INTERNAL', - UserFacing = 'USER_FACING' -} - -export enum EasBuildWaiverType { - FastFailedBuild = 'FAST_FAILED_BUILD', - SystemError = 'SYSTEM_ERROR' -} - -export enum EasService { - Builds = 'BUILDS', - Jobs = 'JOBS', - Updates = 'UPDATES' -} - -export enum EasServiceMetric { - AssetsRequests = 'ASSETS_REQUESTS', - BandwidthUsage = 'BANDWIDTH_USAGE', - Builds = 'BUILDS', - ManifestRequests = 'MANIFEST_REQUESTS', - RunTime = 'RUN_TIME', - UniqueUpdaters = 'UNIQUE_UPDATERS', - UniqueUsers = 'UNIQUE_USERS' -} - -export type EasTotalPlanEnablement = { - __typename?: 'EASTotalPlanEnablement'; - total: Scalars['Int']; - unit?: Maybe; -}; - -export enum EasTotalPlanEnablementUnit { - Build = 'BUILD', - Byte = 'BYTE', - Concurrency = 'CONCURRENCY', - Request = 'REQUEST', - Updater = 'UPDATER', - User = 'USER' -} - -export type EditUpdateBranchInput = { - appId?: InputMaybe; - id?: InputMaybe; - name?: InputMaybe; - newName: Scalars['String']; -}; - -export type EmailSubscriptionMutation = { - __typename?: 'EmailSubscriptionMutation'; - addUser: AddUserPayload; -}; - - -export type EmailSubscriptionMutationAddUserArgs = { - addUserInput: AddUserInput; -}; - -export enum EntityTypeName { - AccountEntity = 'AccountEntity', - AccountSsoConfigurationEntity = 'AccountSSOConfigurationEntity', - AndroidAppCredentialsEntity = 'AndroidAppCredentialsEntity', - AndroidKeystoreEntity = 'AndroidKeystoreEntity', - AppEntity = 'AppEntity', - AppStoreConnectApiKeyEntity = 'AppStoreConnectApiKeyEntity', - AppleDeviceEntity = 'AppleDeviceEntity', - AppleDistributionCertificateEntity = 'AppleDistributionCertificateEntity', - AppleProvisioningProfileEntity = 'AppleProvisioningProfileEntity', - AppleTeamEntity = 'AppleTeamEntity', - BranchEntity = 'BranchEntity', - ChannelEntity = 'ChannelEntity', - CustomerEntity = 'CustomerEntity', - GoogleServiceAccountKeyEntity = 'GoogleServiceAccountKeyEntity', - IosAppCredentialsEntity = 'IosAppCredentialsEntity', - UserInvitationEntity = 'UserInvitationEntity', - UserPermissionEntity = 'UserPermissionEntity', - WorkflowEntity = 'WorkflowEntity', - WorkflowRevisionEntity = 'WorkflowRevisionEntity' -} - -export type EnvironmentSecret = { - __typename?: 'EnvironmentSecret'; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - name: Scalars['String']; - type: EnvironmentSecretType; - updatedAt: Scalars['DateTime']; -}; - -export type EnvironmentSecretMutation = { - __typename?: 'EnvironmentSecretMutation'; - /** Create an environment secret for an Account */ - createEnvironmentSecretForAccount: EnvironmentSecret; - /** Create an environment secret for an App */ - createEnvironmentSecretForApp: EnvironmentSecret; - /** Delete an environment secret */ - deleteEnvironmentSecret: DeleteEnvironmentSecretResult; -}; - - -export type EnvironmentSecretMutationCreateEnvironmentSecretForAccountArgs = { - accountId: Scalars['String']; - environmentSecretData: CreateEnvironmentSecretInput; -}; - - -export type EnvironmentSecretMutationCreateEnvironmentSecretForAppArgs = { - appId: Scalars['String']; - environmentSecretData: CreateEnvironmentSecretInput; -}; - - -export type EnvironmentSecretMutationDeleteEnvironmentSecretArgs = { - id: Scalars['String']; -}; - -export enum EnvironmentSecretType { - FileBase64 = 'FILE_BASE64', - String = 'STRING' -} - -export type EnvironmentVariable = { - __typename?: 'EnvironmentVariable'; - apps: Array; - createdAt: Scalars['DateTime']; - environments?: Maybe>; - fileName?: Maybe; - id: Scalars['ID']; - isGlobal?: Maybe; - linkedEnvironments?: Maybe>; - name: Scalars['String']; - scope: EnvironmentVariableScope; - type: EnvironmentSecretType; - updatedAt: Scalars['DateTime']; - value?: Maybe; - visibility?: Maybe; -}; - - -export type EnvironmentVariableLinkedEnvironmentsArgs = { - appFullName?: InputMaybe; - appId?: InputMaybe; -}; - - -export type EnvironmentVariableValueArgs = { - includeFileContent?: InputMaybe; -}; - -export enum EnvironmentVariableEnvironment { - Development = 'DEVELOPMENT', - Preview = 'PREVIEW', - Production = 'PRODUCTION' -} - -export type EnvironmentVariableMutation = { - __typename?: 'EnvironmentVariableMutation'; - /** Create bulk env variables for an Account */ - createBulkEnvironmentVariablesForAccount: Array; - /** Create bulk env variables for an App */ - createBulkEnvironmentVariablesForApp: Array; - /** Create an environment variable for an Account */ - createEnvironmentVariableForAccount: EnvironmentVariable; - /** Create an environment variable for an App */ - createEnvironmentVariableForApp: EnvironmentVariable; - /** Delete an environment variable */ - deleteEnvironmentVariable: DeleteEnvironmentVariableResult; - /** Bulk link shared environment variables */ - linkBulkSharedEnvironmentVariables: Array; - /** Link shared environment variable */ - linkSharedEnvironmentVariable: EnvironmentVariable; - /** Unlink shared environment variable */ - unlinkSharedEnvironmentVariable: EnvironmentVariable; - /** Update an environment variable */ - updateEnvironmentVariable: EnvironmentVariable; -}; - - -export type EnvironmentVariableMutationCreateBulkEnvironmentVariablesForAccountArgs = { - accountId: Scalars['ID']; - environmentVariablesData: Array; -}; - - -export type EnvironmentVariableMutationCreateBulkEnvironmentVariablesForAppArgs = { - appId: Scalars['ID']; - environmentVariablesData: Array; -}; - - -export type EnvironmentVariableMutationCreateEnvironmentVariableForAccountArgs = { - accountId: Scalars['ID']; - environmentVariableData: CreateSharedEnvironmentVariableInput; -}; - - -export type EnvironmentVariableMutationCreateEnvironmentVariableForAppArgs = { - appId: Scalars['ID']; - environmentVariableData: CreateEnvironmentVariableInput; -}; - - -export type EnvironmentVariableMutationDeleteEnvironmentVariableArgs = { - id: Scalars['ID']; -}; - - -export type EnvironmentVariableMutationLinkBulkSharedEnvironmentVariablesArgs = { - linkData: Array; -}; - - -export type EnvironmentVariableMutationLinkSharedEnvironmentVariableArgs = { - appId: Scalars['ID']; - environment?: InputMaybe; - environmentVariableId: Scalars['ID']; -}; - - -export type EnvironmentVariableMutationUnlinkSharedEnvironmentVariableArgs = { - appId: Scalars['ID']; - environment?: InputMaybe; - environmentVariableId: Scalars['ID']; -}; - - -export type EnvironmentVariableMutationUpdateEnvironmentVariableArgs = { - environmentVariableData: UpdateEnvironmentVariableInput; -}; - -export enum EnvironmentVariableScope { - Project = 'PROJECT', - Shared = 'SHARED' -} - -export enum EnvironmentVariableVisibility { - Public = 'PUBLIC', - Secret = 'SECRET', - Sensitive = 'SENSITIVE' -} - -export type EnvironmentVariableWithSecret = { - __typename?: 'EnvironmentVariableWithSecret'; - apps: Array; - createdAt: Scalars['DateTime']; - environments?: Maybe>; - fileName?: Maybe; - id: Scalars['ID']; - isGlobal: Scalars['Boolean']; - linkedEnvironments?: Maybe>; - name: Scalars['String']; - scope: EnvironmentVariableScope; - sensitive: Scalars['Boolean']; - type: EnvironmentSecretType; - updatedAt: Scalars['DateTime']; - value?: Maybe; - visibility: EnvironmentVariableVisibility; -}; - - -export type EnvironmentVariableWithSecretLinkedEnvironmentsArgs = { - appFullName?: InputMaybe; - appId?: InputMaybe; -}; - - -export type EnvironmentVariableWithSecretValueArgs = { - includeFileContent?: InputMaybe; -}; - -export type EstimatedOverageAndCost = { - __typename?: 'EstimatedOverageAndCost'; - id: Scalars['ID']; - /** The limit, in units, allowed by this plan */ - limit: Scalars['Float']; - metadata?: Maybe; - metricType: UsageMetricType; - service: EasService; - serviceMetric: EasServiceMetric; - /** Total cost of this particular metric, in cents */ - totalCost: Scalars['Int']; - value: Scalars['Float']; -}; - -export type EstimatedUsage = { - __typename?: 'EstimatedUsage'; - id: Scalars['ID']; - limit: Scalars['Float']; - metricType: UsageMetricType; - service: EasService; - serviceMetric: EasServiceMetric; - value: Scalars['Float']; -}; - -export enum Experiment { - Orbit = 'ORBIT' -} - -export type ExperimentationQuery = { - __typename?: 'ExperimentationQuery'; - /** Get device experimentation config */ - deviceConfig: Scalars['JSONObject']; - /** Get experimentation unit to use for device experiments. In this case, it is the IP address. */ - deviceExperimentationUnit: Scalars['ID']; - /** Get user experimentation config */ - userConfig: Scalars['JSONObject']; -}; - -export type FcmSnippet = FcmSnippetLegacy | FcmSnippetV1; - -export type FcmSnippetLegacy = { - __typename?: 'FcmSnippetLegacy'; - firstFourCharacters: Scalars['String']; - lastFourCharacters: Scalars['String']; -}; - -export type FcmSnippetV1 = { - __typename?: 'FcmSnippetV1'; - clientId?: Maybe; - keyId: Scalars['String']; - projectId: Scalars['String']; - serviceAccountEmail: Scalars['String']; -}; - -export enum Feature { - /** Priority Builds */ - Builds = 'BUILDS', - /** Funds support for open source development */ - OpenSource = 'OPEN_SOURCE', - /** Top Tier Support */ - Support = 'SUPPORT', - /** Share access to projects */ - Teams = 'TEAMS' -} - -export type FingerprintSourceInput = { - bucketKey?: InputMaybe; - isDebugFingerprint?: InputMaybe; - type?: InputMaybe; -}; - -export enum FingerprintSourceType { - Gcs = 'GCS' -} - -export type FutureSubscription = { - __typename?: 'FutureSubscription'; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - meteredBillingStatus: MeteredBillingStatus; - planId: Scalars['String']; - recurringCents?: Maybe; - startDate: Scalars['DateTime']; -}; - -export type GetSignedAssetUploadSpecificationsResult = { - __typename?: 'GetSignedAssetUploadSpecificationsResult'; - specifications: Array; -}; - -export enum GitHubAppEnvironment { - Development = 'DEVELOPMENT', - Production = 'PRODUCTION', - Staging = 'STAGING' -} - -export type GitHubAppInstallation = { - __typename?: 'GitHubAppInstallation'; - account: Account; - actor?: Maybe; - id: Scalars['ID']; - installationIdentifier: Scalars['Int']; - metadata: GitHubAppInstallationMetadata; -}; - -export type GitHubAppInstallationAccessibleRepository = { - __typename?: 'GitHubAppInstallationAccessibleRepository'; - defaultBranch?: Maybe; - description?: Maybe; - id: Scalars['Int']; - name: Scalars['String']; - nodeId: Scalars['String']; - owner: GitHubRepositoryOwner; - private: Scalars['Boolean']; - url: Scalars['String']; -}; - -export type GitHubAppInstallationMetadata = { - __typename?: 'GitHubAppInstallationMetadata'; - githubAccountAvatarUrl?: Maybe; - githubAccountName?: Maybe; - installationStatus: GitHubAppInstallationStatus; -}; - -export type GitHubAppInstallationMutation = { - __typename?: 'GitHubAppInstallationMutation'; - /** Create a GitHub App installation for an Account */ - createGitHubAppInstallationForAccount: GitHubAppInstallation; - /** Delete a GitHub App installation by ID */ - deleteGitHubAppInstallation: GitHubAppInstallation; -}; - - -export type GitHubAppInstallationMutationCreateGitHubAppInstallationForAccountArgs = { - githubAppInstallationData: CreateGitHubAppInstallationInput; -}; - - -export type GitHubAppInstallationMutationDeleteGitHubAppInstallationArgs = { - githubAppInstallationId: Scalars['ID']; -}; - -export enum GitHubAppInstallationStatus { - Active = 'ACTIVE', - NotInstalled = 'NOT_INSTALLED', - Suspended = 'SUSPENDED' -} - -export type GitHubAppMutation = { - __typename?: 'GitHubAppMutation'; - /** Create a GitHub build for an app. Returns the ID of the background job receipt. Use BackgroundJobReceiptQuery to get the status of the job. */ - createGitHubBuild: BackgroundJobReceipt; -}; - - -export type GitHubAppMutationCreateGitHubBuildArgs = { - buildInput: GitHubBuildInput; -}; - -export type GitHubAppQuery = { - __typename?: 'GitHubAppQuery'; - appIdentifier: Scalars['String']; - clientIdentifier: Scalars['String']; - environment: GitHubAppEnvironment; - installation: GitHubAppInstallation; - name: Scalars['String']; -}; - - -export type GitHubAppQueryInstallationArgs = { - id: Scalars['ID']; -}; - -export type GitHubBuildInput = { - appId: Scalars['ID']; - autoSubmit?: InputMaybe; - baseDirectory?: InputMaybe; - buildProfile: Scalars['String']; - environment?: InputMaybe; - gitRef: Scalars['String']; - platform: AppPlatform; - /** Repack the golden dev client build instead of running full build process. Used for onboarding. Do not use outside of onboarding flow, as for now it's only created with this specific use case in mind. */ - repack?: InputMaybe; - submitProfile?: InputMaybe; -}; - -export type GitHubBuildTrigger = { - __typename?: 'GitHubBuildTrigger'; - app: App; - autoSubmit: Scalars['Boolean']; - buildProfile: Scalars['String']; - createdAt: Scalars['DateTime']; - environment?: Maybe; - executionBehavior: GitHubBuildTriggerExecutionBehavior; - id: Scalars['ID']; - isActive: Scalars['Boolean']; - lastRunAt?: Maybe; - lastRunBuild?: Maybe; - lastRunErrorCode?: Maybe; - lastRunErrorMessage?: Maybe; - lastRunStatus?: Maybe; - platform: AppPlatform; - sourcePattern: Scalars['String']; - submitProfile?: Maybe; - targetPattern?: Maybe; - type: GitHubBuildTriggerType; - updatedAt: Scalars['DateTime']; -}; - -export enum GitHubBuildTriggerExecutionBehavior { - Always = 'ALWAYS', - BaseDirectoryChanged = 'BASE_DIRECTORY_CHANGED' -} - -export type GitHubBuildTriggerMutation = { - __typename?: 'GitHubBuildTriggerMutation'; - /** Create GitHub build trigger for an App */ - createGitHubBuildTrigger: GitHubBuildTrigger; - /** Delete GitHub build trigger by ID */ - deleteGitHubBuildTrigger: GitHubBuildTrigger; - /** Update a GitHub build trigger by ID */ - updateGitHubBuildTrigger: GitHubBuildTrigger; -}; - - -export type GitHubBuildTriggerMutationCreateGitHubBuildTriggerArgs = { - githubBuildTriggerData: CreateGitHubBuildTriggerInput; -}; - - -export type GitHubBuildTriggerMutationDeleteGitHubBuildTriggerArgs = { - githubBuildTriggerId: Scalars['ID']; -}; - - -export type GitHubBuildTriggerMutationUpdateGitHubBuildTriggerArgs = { - githubBuildTriggerData: UpdateGitHubBuildTriggerInput; - githubBuildTriggerId: Scalars['ID']; -}; - -export enum GitHubBuildTriggerRunStatus { - Errored = 'ERRORED', - Success = 'SUCCESS' -} - -export enum GitHubBuildTriggerType { - PullRequestUpdated = 'PULL_REQUEST_UPDATED', - PushToBranch = 'PUSH_TO_BRANCH', - TagUpdated = 'TAG_UPDATED' -} - -export enum GitHubJobRunJobType { - PublishUpdate = 'PUBLISH_UPDATE' -} - -export type GitHubJobRunTrigger = { - __typename?: 'GitHubJobRunTrigger'; - app: App; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - isActive: Scalars['Boolean']; - jobType?: Maybe; - lastRunAt?: Maybe; - lastRunErrorCode?: Maybe; - lastRunErrorMessage?: Maybe; - lastRunStatus?: Maybe; - sourcePattern: Scalars['String']; - targetPattern?: Maybe; - triggerType: GitHubJobRunTriggerType; -}; - -export type GitHubJobRunTriggerMutation = { - __typename?: 'GitHubJobRunTriggerMutation'; - createGitHubJobRunTrigger: GitHubJobRunTrigger; - deleteGitHubJobRunTrigger: GitHubJobRunTrigger; - updateGitHubJobRunTrigger: GitHubJobRunTrigger; -}; - - -export type GitHubJobRunTriggerMutationCreateGitHubJobRunTriggerArgs = { - gitHubJobRunTriggerData: CreateGitHubJobRunTriggerInput; -}; - - -export type GitHubJobRunTriggerMutationDeleteGitHubJobRunTriggerArgs = { - gitHubJobRunTriggerId: Scalars['ID']; -}; - - -export type GitHubJobRunTriggerMutationUpdateGitHubJobRunTriggerArgs = { - gitHubJobRunTriggerData: UpdateGitHubJobRunTriggerInput; - gitHubJobRunTriggerId: Scalars['ID']; -}; - -export enum GitHubJobRunTriggerRunStatus { - Errored = 'ERRORED', - Success = 'SUCCESS' -} - -export enum GitHubJobRunTriggerType { - PullRequestUpdated = 'PULL_REQUEST_UPDATED', - PushToBranch = 'PUSH_TO_BRANCH' -} - -export type GitHubRepository = { - __typename?: 'GitHubRepository'; - app: App; - createdAt: Scalars['DateTime']; - githubAppInstallation: GitHubAppInstallation; - githubRepositoryIdentifier: Scalars['Int']; - githubRepositoryUrl?: Maybe; - id: Scalars['ID']; - metadata: GitHubRepositoryMetadata; - nodeIdentifier: Scalars['String']; -}; - -export type GitHubRepositoryMetadata = { - __typename?: 'GitHubRepositoryMetadata'; - defaultBranch?: Maybe; - githubRepoDescription?: Maybe; - githubRepoName: Scalars['String']; - githubRepoOwnerName: Scalars['String']; - githubRepoUrl: Scalars['String']; - lastPushed: Scalars['DateTime']; - lastUpdated: Scalars['DateTime']; - private: Scalars['Boolean']; -}; - -export type GitHubRepositoryMutation = { - __typename?: 'GitHubRepositoryMutation'; - /** Configure EAS by pushing a commit to the default branch which updates or creates app.json, eas.json, and installs necessary dependencies. */ - configureEAS: BackgroundJobReceipt; - /** Create a GitHub repository for an App */ - createGitHubRepository: GitHubRepository; - /** Delete a GitHub repository by ID */ - deleteGitHubRepository: GitHubRepository; -}; - - -export type GitHubRepositoryMutationConfigureEasArgs = { - githubRepositoryId: Scalars['ID']; -}; - - -export type GitHubRepositoryMutationCreateGitHubRepositoryArgs = { - githubRepositoryData: CreateGitHubRepositoryInput; -}; - - -export type GitHubRepositoryMutationDeleteGitHubRepositoryArgs = { - githubRepositoryId: Scalars['ID']; -}; - -export type GitHubRepositoryOwner = { - __typename?: 'GitHubRepositoryOwner'; - avatarUrl: Scalars['String']; - id: Scalars['Int']; - login: Scalars['String']; - url: Scalars['String']; -}; - -export type GitHubRepositoryPaginationResult = { - __typename?: 'GitHubRepositoryPaginationResult'; - repositories: Array; - totalCount: Scalars['Int']; -}; - -export type GitHubRepositorySettings = { - __typename?: 'GitHubRepositorySettings'; - app: App; - baseDirectory: Scalars['String']; - id: Scalars['ID']; -}; - -export type GitHubRepositorySettingsMutation = { - __typename?: 'GitHubRepositorySettingsMutation'; - /** Create GitHub repository settings for an App */ - createGitHubRepositorySettings: GitHubRepositorySettings; - /** Delete GitHub repository settings by ID */ - deleteGitHubRepositorySettings: GitHubRepositorySettings; - /** Update GitHub repository settings */ - updateGitHubRepositorySettings: GitHubRepositorySettings; -}; - - -export type GitHubRepositorySettingsMutationCreateGitHubRepositorySettingsArgs = { - githubRepositorySettingsData: CreateGitHubRepositorySettingsInput; -}; - - -export type GitHubRepositorySettingsMutationDeleteGitHubRepositorySettingsArgs = { - githubRepositorySettingsId: Scalars['ID']; -}; - - -export type GitHubRepositorySettingsMutationUpdateGitHubRepositorySettingsArgs = { - githubRepositorySettingsData: UpdateGitHubRepositorySettingsInput; - githubRepositorySettingsId: Scalars['ID']; -}; - -export type GitHubUser = { - __typename?: 'GitHubUser'; - githubUserIdentifier: Scalars['String']; - id: Scalars['ID']; - metadata?: Maybe; - userActor: UserActor; -}; - -export type GitHubUserMetadata = { - __typename?: 'GitHubUserMetadata'; - avatarUrl: Scalars['String']; - login: Scalars['String']; - name?: Maybe; - url: Scalars['String']; -}; - -export type GitHubUserMutation = { - __typename?: 'GitHubUserMutation'; - /** Delete a GitHub User by ID */ - deleteGitHubUser: DeleteGitHubUserResult; - /** Generate a GitHub User Access Token */ - generateGitHubUserAccessToken?: Maybe; -}; - - -export type GitHubUserMutationDeleteGitHubUserArgs = { - id: Scalars['ID']; -}; - -export type GoogleServiceAccountKey = { - __typename?: 'GoogleServiceAccountKey'; - account: Account; - clientEmail: Scalars['String']; - clientIdentifier: Scalars['String']; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - keyJson: Scalars['String']; - privateKeyIdentifier: Scalars['String']; - projectIdentifier: Scalars['String']; - updatedAt: Scalars['DateTime']; -}; - -export type GoogleServiceAccountKeyInput = { - jsonKey: Scalars['JSONObject']; -}; - -export type GoogleServiceAccountKeyMutation = { - __typename?: 'GoogleServiceAccountKeyMutation'; - /** Create a Google Service Account Key */ - createGoogleServiceAccountKey: GoogleServiceAccountKey; - /** Delete a Google Service Account Key */ - deleteGoogleServiceAccountKey: DeleteGoogleServiceAccountKeyResult; -}; - - -export type GoogleServiceAccountKeyMutationCreateGoogleServiceAccountKeyArgs = { - accountId: Scalars['ID']; - googleServiceAccountKeyInput: GoogleServiceAccountKeyInput; -}; - - -export type GoogleServiceAccountKeyMutationDeleteGoogleServiceAccountKeyArgs = { - id: Scalars['ID']; -}; - -export type GoogleServiceAccountKeyQuery = { - __typename?: 'GoogleServiceAccountKeyQuery'; - byId: GoogleServiceAccountKey; -}; - - -export type GoogleServiceAccountKeyQueryByIdArgs = { - id: Scalars['ID']; -}; - -/** - * The value field is always sent from the client as a string, - * and then it's parsed server-side according to the filterType - */ -export type InsightsFilter = { - filterType: InsightsFilterType; - value: Scalars['String']; -}; - -export enum InsightsFilterType { - Platform = 'PLATFORM' -} - -export type InsightsTimespan = { - end: Scalars['DateTime']; - start: Scalars['DateTime']; -}; - -export type Invoice = { - __typename?: 'Invoice'; - /** The total amount due for the invoice, in cents */ - amountDue: Scalars['Int']; - /** The total amount that has been paid, considering any discounts or account credit. Value is in cents. */ - amountPaid: Scalars['Int']; - /** The total amount that needs to be paid, considering any discounts or account credit. Value is in cents. */ - amountRemaining: Scalars['Int']; - discount?: Maybe; - id: Scalars['ID']; - lineItems: Array; - period: InvoicePeriod; - startingBalance: Scalars['Int']; - subtotal: Scalars['Int']; - total: Scalars['Int']; - totalDiscountedAmount: Scalars['Int']; -}; - -export type InvoiceDiscount = { - __typename?: 'InvoiceDiscount'; - /** The coupon's discount value, in percentage or in dollar amount */ - amount: Scalars['Int']; - duration: Scalars['String']; - durationInMonths?: Maybe; - id: Scalars['ID']; - name: Scalars['String']; - type: InvoiceDiscountType; -}; - -export enum InvoiceDiscountType { - Amount = 'AMOUNT', - Percentage = 'PERCENTAGE' -} - -export type InvoiceLineItem = { - __typename?: 'InvoiceLineItem'; - /** Line-item amount in cents */ - amount: Scalars['Int']; - description: Scalars['String']; - id: Scalars['ID']; - metadata: Scalars['JSONObject']; - period: InvoicePeriod; - /** @deprecated Use 'price' instead */ - plan: InvoiceLineItemPlan; - price?: Maybe; - proration: Scalars['Boolean']; - quantity: Scalars['Int']; - /** The unit amount excluding tax, in cents */ - unitAmountExcludingTax?: Maybe; -}; - -export type InvoiceLineItemPlan = { - __typename?: 'InvoiceLineItemPlan'; - id: Scalars['ID']; - name?: Maybe; -}; - -export type InvoicePeriod = { - __typename?: 'InvoicePeriod'; - end: Scalars['DateTime']; - start: Scalars['DateTime']; -}; - -export type InvoiceQuery = { - __typename?: 'InvoiceQuery'; - /** - * Previews the invoice for the specified number of additional concurrencies. - * This is the total number of concurrencies the customer wishes to purchase - * on top of their base plan, not the relative change in concurrencies - * the customer wishes to make. For example, specify "3" if the customer has - * two add-on concurrencies and wishes to purchase one more. - */ - previewInvoiceForAdditionalConcurrenciesCountUpdate?: Maybe; - /** Preview an upgrade subscription invoice, with proration */ - previewInvoiceForSubscriptionUpdate: Invoice; -}; - - -export type InvoiceQueryPreviewInvoiceForAdditionalConcurrenciesCountUpdateArgs = { - accountID: Scalars['ID']; - additionalConcurrenciesCount: Scalars['Int']; -}; - - -export type InvoiceQueryPreviewInvoiceForSubscriptionUpdateArgs = { - accountId: Scalars['String']; - couponCode?: InputMaybe; - newPlanIdentifier: Scalars['String']; -}; - -export type IosAppBuildCredentials = { - __typename?: 'IosAppBuildCredentials'; - /** @deprecated Get Apple Devices from AppleProvisioningProfile instead */ - appleDevices?: Maybe>>; - distributionCertificate?: Maybe; - id: Scalars['ID']; - iosAppCredentials: IosAppCredentials; - iosDistributionType: IosDistributionType; - provisioningProfile?: Maybe; -}; - -export type IosAppBuildCredentialsFilter = { - iosDistributionType?: InputMaybe; -}; - -export type IosAppBuildCredentialsInput = { - distributionCertificateId: Scalars['ID']; - iosDistributionType: IosDistributionType; - provisioningProfileId: Scalars['ID']; -}; - -export type IosAppBuildCredentialsMutation = { - __typename?: 'IosAppBuildCredentialsMutation'; - /** Create a set of build credentials for an iOS app */ - createIosAppBuildCredentials: IosAppBuildCredentials; - /** Disassociate the build credentials from an iOS app */ - deleteIosAppBuildCredentials: DeleteIosAppBuildCredentialsResult; - /** Set the distribution certificate to be used for an iOS app */ - setDistributionCertificate: IosAppBuildCredentials; - /** Set the provisioning profile to be used for an iOS app */ - setProvisioningProfile: IosAppBuildCredentials; -}; - - -export type IosAppBuildCredentialsMutationCreateIosAppBuildCredentialsArgs = { - iosAppBuildCredentialsInput: IosAppBuildCredentialsInput; - iosAppCredentialsId: Scalars['ID']; -}; - - -export type IosAppBuildCredentialsMutationDeleteIosAppBuildCredentialsArgs = { - id: Scalars['ID']; -}; - - -export type IosAppBuildCredentialsMutationSetDistributionCertificateArgs = { - distributionCertificateId: Scalars['ID']; - id: Scalars['ID']; -}; - - -export type IosAppBuildCredentialsMutationSetProvisioningProfileArgs = { - id: Scalars['ID']; - provisioningProfileId: Scalars['ID']; -}; - -export type IosAppCredentials = { - __typename?: 'IosAppCredentials'; - app: App; - appStoreConnectApiKeyForBuilds?: Maybe; - appStoreConnectApiKeyForSubmissions?: Maybe; - appleAppIdentifier: AppleAppIdentifier; - appleTeam?: Maybe; - id: Scalars['ID']; - /** @deprecated use iosAppBuildCredentialsList instead */ - iosAppBuildCredentialsArray: Array; - iosAppBuildCredentialsList: Array; - pushKey?: Maybe; -}; - - -export type IosAppCredentialsIosAppBuildCredentialsArrayArgs = { - filter?: InputMaybe; -}; - - -export type IosAppCredentialsIosAppBuildCredentialsListArgs = { - filter?: InputMaybe; -}; - -export type IosAppCredentialsFilter = { - appleAppIdentifierId?: InputMaybe; -}; - -export type IosAppCredentialsInput = { - appStoreConnectApiKeyForBuildsId?: InputMaybe; - appStoreConnectApiKeyForSubmissionsId?: InputMaybe; - appleTeamId?: InputMaybe; - pushKeyId?: InputMaybe; -}; - -export type IosAppCredentialsMutation = { - __typename?: 'IosAppCredentialsMutation'; - /** Create a set of credentials for an iOS app */ - createIosAppCredentials: IosAppCredentials; - /** Delete a set of credentials for an iOS app */ - deleteIosAppCredentials: DeleteIosAppCredentialsResult; - /** Set the App Store Connect Api Key to be used for submitting an iOS app */ - setAppStoreConnectApiKeyForSubmissions: IosAppCredentials; - /** Set the push key to be used in an iOS app */ - setPushKey: IosAppCredentials; - /** Update a set of credentials for an iOS app */ - updateIosAppCredentials: IosAppCredentials; -}; - - -export type IosAppCredentialsMutationCreateIosAppCredentialsArgs = { - appId: Scalars['ID']; - appleAppIdentifierId: Scalars['ID']; - iosAppCredentialsInput: IosAppCredentialsInput; -}; - - -export type IosAppCredentialsMutationDeleteIosAppCredentialsArgs = { - id: Scalars['ID']; -}; - - -export type IosAppCredentialsMutationSetAppStoreConnectApiKeyForSubmissionsArgs = { - ascApiKeyId: Scalars['ID']; - id: Scalars['ID']; -}; - - -export type IosAppCredentialsMutationSetPushKeyArgs = { - id: Scalars['ID']; - pushKeyId: Scalars['ID']; -}; - - -export type IosAppCredentialsMutationUpdateIosAppCredentialsArgs = { - id: Scalars['ID']; - iosAppCredentialsInput: IosAppCredentialsInput; -}; - -/** @deprecated Use developmentClient option instead. */ -export enum IosBuildType { - DevelopmentClient = 'DEVELOPMENT_CLIENT', - Release = 'RELEASE' -} - -export type IosBuilderEnvironmentInput = { - bun?: InputMaybe; - bundler?: InputMaybe; - cocoapods?: InputMaybe; - env?: InputMaybe; - expoCli?: InputMaybe; - fastlane?: InputMaybe; - image?: InputMaybe; - node?: InputMaybe; - pnpm?: InputMaybe; - yarn?: InputMaybe; -}; - -export enum IosDistributionType { - AdHoc = 'AD_HOC', - AppStore = 'APP_STORE', - Development = 'DEVELOPMENT', - Enterprise = 'ENTERPRISE' -} - -export type IosJobDistributionCertificateInput = { - dataBase64: Scalars['String']; - password: Scalars['String']; -}; - -export type IosJobInput = { - applicationArchivePath?: InputMaybe; - /** @deprecated */ - artifactPath?: InputMaybe; - buildArtifactPaths?: InputMaybe>; - buildConfiguration?: InputMaybe; - buildProfile?: InputMaybe; - /** @deprecated */ - buildType?: InputMaybe; - builderEnvironment?: InputMaybe; - cache?: InputMaybe; - customBuildConfig?: InputMaybe; - developmentClient?: InputMaybe; - /** @deprecated */ - distribution?: InputMaybe; - experimental?: InputMaybe; - loggerLevel?: InputMaybe; - mode?: InputMaybe; - projectArchive: ProjectArchiveSourceInput; - projectRootDirectory: Scalars['String']; - releaseChannel?: InputMaybe; - scheme?: InputMaybe; - secrets?: InputMaybe; - simulator?: InputMaybe; - triggeredBy?: InputMaybe; - type: BuildWorkflow; - updates?: InputMaybe; - username?: InputMaybe; - version?: InputMaybe; -}; - -export type IosJobOverridesInput = { - applicationArchivePath?: InputMaybe; - /** @deprecated */ - artifactPath?: InputMaybe; - buildArtifactPaths?: InputMaybe>; - buildConfiguration?: InputMaybe; - buildProfile?: InputMaybe; - /** @deprecated */ - buildType?: InputMaybe; - builderEnvironment?: InputMaybe; - cache?: InputMaybe; - customBuildConfig?: InputMaybe; - developmentClient?: InputMaybe; - /** @deprecated */ - distribution?: InputMaybe; - experimental?: InputMaybe; - loggerLevel?: InputMaybe; - mode?: InputMaybe; - releaseChannel?: InputMaybe; - resign?: InputMaybe; - scheme?: InputMaybe; - secrets?: InputMaybe; - simulator?: InputMaybe; - type?: InputMaybe; - updates?: InputMaybe; - username?: InputMaybe; - version?: InputMaybe; -}; - -export type IosJobSecretsInput = { - buildCredentials?: InputMaybe>>; - robotAccessToken?: InputMaybe; -}; - -export type IosJobTargetCredentialsInput = { - distributionCertificate: IosJobDistributionCertificateInput; - provisioningProfileBase64: Scalars['String']; - targetName: Scalars['String']; -}; - -export type IosJobVersionInput = { - buildNumber: Scalars['String']; -}; - -/** @deprecated Use developmentClient option instead. */ -export enum IosManagedBuildType { - DevelopmentClient = 'DEVELOPMENT_CLIENT', - Release = 'RELEASE' -} - -export enum IosSchemeBuildConfiguration { - Debug = 'DEBUG', - Release = 'RELEASE' -} - -export type IosSubmissionConfig = { - __typename?: 'IosSubmissionConfig'; - appleIdUsername?: Maybe; - ascApiKeyId?: Maybe; - ascAppIdentifier: Scalars['String']; -}; - -export type IosSubmissionConfigInput = { - appleAppSpecificPassword?: InputMaybe; - appleIdUsername?: InputMaybe; - archiveUrl?: InputMaybe; - ascApiKey?: InputMaybe; - ascApiKeyId?: InputMaybe; - ascAppIdentifier: Scalars['String']; - isVerboseFastlaneEnabled?: InputMaybe; -}; - -/** Represents a Turtle Job Run */ -export type JobRun = { - __typename?: 'JobRun'; - app: App; - /** @deprecated No longer supported */ - childJobRun?: Maybe; - createdAt: Scalars['DateTime']; - displayName?: Maybe; - endedAt?: Maybe; - expiresAt: Scalars['DateTime']; - gitCommitHash?: Maybe; - gitCommitMessage?: Maybe; - gitRef?: Maybe; - id: Scalars['ID']; - initiatingActor?: Maybe; - isWaived: Scalars['Boolean']; - logFileUrls: Array; - name: Scalars['String']; - priority: JobRunPriority; - startedAt?: Maybe; - status: JobRunStatus; - updateGroups: Array>; -}; - -export type JobRunMutation = { - __typename?: 'JobRunMutation'; - /** Cancel an EAS Job Run */ - cancelJobRun: JobRun; -}; - - -export type JobRunMutationCancelJobRunArgs = { - jobRunId: Scalars['ID']; -}; - -export enum JobRunPriority { - High = 'HIGH', - Normal = 'NORMAL' -} - -export type JobRunQuery = { - __typename?: 'JobRunQuery'; - /** Look up EAS Job Run by ID */ - byId: JobRun; -}; - - -export type JobRunQueryByIdArgs = { - jobRunId: Scalars['ID']; -}; - -export enum JobRunStatus { - Canceled = 'CANCELED', - Errored = 'ERRORED', - Finished = 'FINISHED', - InProgress = 'IN_PROGRESS', - InQueue = 'IN_QUEUE', - New = 'NEW', - PendingCancel = 'PENDING_CANCEL' -} - -export type KeystoreGenerationUrl = { - __typename?: 'KeystoreGenerationUrl'; - id: Scalars['ID']; - url: Scalars['String']; -}; - -export type KeystoreGenerationUrlMutation = { - __typename?: 'KeystoreGenerationUrlMutation'; - /** Create a Keystore Generation URL */ - createKeystoreGenerationUrl: KeystoreGenerationUrl; -}; - -export type LatestUpdateOnBranch = { - __typename?: 'LatestUpdateOnBranch'; - branchId: Scalars['String']; - update?: Maybe; -}; - -export type LeaveAccountResult = { - __typename?: 'LeaveAccountResult'; - success: Scalars['Boolean']; -}; - -export type LineChartData = { - __typename?: 'LineChartData'; - datasets: Array; - labels: Array; -}; - -export type LineDatapoint = { - __typename?: 'LineDatapoint'; - data: Scalars['Int']; - id: Scalars['ID']; - label: Scalars['String']; -}; - -export type LineDataset = { - __typename?: 'LineDataset'; - data: Array>; - id: Scalars['ID']; - label: Scalars['String']; -}; - -export type LinkSharedEnvironmentVariableInput = { - appId: Scalars['ID']; - environment?: InputMaybe; - environmentVariableId: Scalars['ID']; -}; - -export type LogNameTypeMapping = { - __typename?: 'LogNameTypeMapping'; - publicName: Scalars['String']; - typeName: EntityTypeName; -}; - -export type LogsTimespan = { - end: Scalars['DateTime']; - start?: InputMaybe; -}; - -export enum MailchimpAudience { - ExpoDevelopers = 'EXPO_DEVELOPERS', - ExpoDeveloperOnboarding = 'EXPO_DEVELOPER_ONBOARDING', - LaunchParty_2024 = 'LAUNCH_PARTY_2024', - NonprodExpoDevelopers = 'NONPROD_EXPO_DEVELOPERS' -} - -export enum MailchimpTag { - DevClientUsers = 'DEV_CLIENT_USERS', - DidSubscribeToEasAtLeastOnce = 'DID_SUBSCRIBE_TO_EAS_AT_LEAST_ONCE', - EasMasterList = 'EAS_MASTER_LIST', - NewsletterSignupList = 'NEWSLETTER_SIGNUP_LIST' -} - -export type MailchimpTagPayload = { - __typename?: 'MailchimpTagPayload'; - id?: Maybe; - name?: Maybe; -}; - -export type MeMutation = { - __typename?: 'MeMutation'; - /** Add an additional second factor device */ - addSecondFactorDevice: SecondFactorDeviceConfigurationResult; - /** Certify an initiated second factor authentication method for the current user */ - certifySecondFactorDevice: SecondFactorBooleanResult; - /** Create a new Account and grant this User the owner Role */ - createAccount: Account; - /** Delete a second factor device */ - deleteSecondFactorDevice: SecondFactorBooleanResult; - /** Delete a Snack that the current user owns */ - deleteSnack: Snack; - /** Disable all second factor authentication for the current user */ - disableSecondFactorAuthentication: SecondFactorBooleanResult; - /** Initiate setup of two-factor authentication for the current user */ - initiateSecondFactorAuthentication: SecondFactorInitiationResult; - /** Leave an Account (revoke own permissions on Account) */ - leaveAccount: LeaveAccountResult; - /** Purge unfinished two-factor authentication setup for the current user if not fully-set-up */ - purgeUnfinishedSecondFactorAuthentication: SecondFactorBooleanResult; - /** Regenerate backup codes for the current user */ - regenerateSecondFactorBackupCodes: SecondFactorRegenerateBackupCodesResult; - /** Schedule deletion for Account created via createAccount */ - scheduleAccountDeletion: BackgroundJobReceipt; - /** Schedule deletion of the current regular user */ - scheduleCurrentUserDeletion: BackgroundJobReceipt; - /** Schedule deletion of a SSO user. Actor must be an owner on the SSO user's SSO account. */ - scheduleSSOUserDeletionAsSSOAccountOwner: BackgroundJobReceipt; - /** Send SMS OTP to a second factor device for use during device setup or during change confirmation */ - sendSMSOTPToSecondFactorDevice: SecondFactorBooleanResult; - /** - * Sets user preferences. This is a key-value store for user-specific settings. Provided values are - * key-level merged with existing values. - */ - setPreferences: UserPreferences; - /** Set the user's primary second factor device */ - setPrimarySecondFactorDevice: SecondFactorBooleanResult; - /** Transfer project to a different Account */ - transferApp: App; - /** Update an App that the current user owns */ - updateApp: App; - /** Update the current regular user's data */ - updateProfile: User; - /** Update the current SSO user's data */ - updateSSOProfile: SsoUser; -}; - - -export type MeMutationAddSecondFactorDeviceArgs = { - deviceConfiguration: SecondFactorDeviceConfiguration; - otp?: InputMaybe; -}; - - -export type MeMutationCertifySecondFactorDeviceArgs = { - otp: Scalars['String']; -}; - - -export type MeMutationCreateAccountArgs = { - accountData: AccountDataInput; -}; - - -export type MeMutationDeleteSecondFactorDeviceArgs = { - otp?: InputMaybe; - userSecondFactorDeviceId: Scalars['ID']; -}; - - -export type MeMutationDeleteSnackArgs = { - snackId: Scalars['ID']; -}; - - -export type MeMutationDisableSecondFactorAuthenticationArgs = { - otp?: InputMaybe; -}; - - -export type MeMutationInitiateSecondFactorAuthenticationArgs = { - deviceConfigurations: Array; - recaptchaResponseToken?: InputMaybe; -}; - - -export type MeMutationLeaveAccountArgs = { - accountId: Scalars['ID']; -}; - - -export type MeMutationRegenerateSecondFactorBackupCodesArgs = { - otp?: InputMaybe; -}; - - -export type MeMutationScheduleAccountDeletionArgs = { - accountId: Scalars['ID']; -}; - - -export type MeMutationScheduleSsoUserDeletionAsSsoAccountOwnerArgs = { - ssoUserId: Scalars['ID']; -}; - - -export type MeMutationSendSmsotpToSecondFactorDeviceArgs = { - userSecondFactorDeviceId: Scalars['ID']; -}; - - -export type MeMutationSetPreferencesArgs = { - preferences: UserPreferencesInput; -}; - - -export type MeMutationSetPrimarySecondFactorDeviceArgs = { - userSecondFactorDeviceId: Scalars['ID']; -}; - - -export type MeMutationTransferAppArgs = { - appId: Scalars['ID']; - destinationAccountId: Scalars['ID']; -}; - - -export type MeMutationUpdateAppArgs = { - appData: AppDataInput; -}; - - -export type MeMutationUpdateProfileArgs = { - userData: UserDataInput; -}; - - -export type MeMutationUpdateSsoProfileArgs = { - userData: SsoUserDataInput; -}; - -export type MeteredBillingStatus = { - __typename?: 'MeteredBillingStatus'; - EAS_BUILD: Scalars['Boolean']; - EAS_UPDATE: Scalars['Boolean']; -}; - -export type Notification = { - __typename?: 'Notification'; - accountName: Scalars['String']; - createdAt: Scalars['DateTime']; - event: NotificationEvent; - id: Scalars['ID']; - isRead: Scalars['Boolean']; - metadata?: Maybe; - type: NotificationType; - updatedAt: Scalars['DateTime']; - websiteMessage: Scalars['String']; -}; - -export enum NotificationEvent { - BuildComplete = 'BUILD_COMPLETE', - BuildErrored = 'BUILD_ERRORED', - BuildLimitThresholdExceeded = 'BUILD_LIMIT_THRESHOLD_EXCEEDED', - BuildPlanCreditThresholdExceeded = 'BUILD_PLAN_CREDIT_THRESHOLD_EXCEEDED', - SubmissionComplete = 'SUBMISSION_COMPLETE', - SubmissionErrored = 'SUBMISSION_ERRORED', - Test = 'TEST' -} - -export type NotificationMetadata = BuildLimitThresholdExceededMetadata | BuildPlanCreditThresholdExceededMetadata | TestNotificationMetadata; - -export type NotificationSubscription = { - __typename?: 'NotificationSubscription'; - account?: Maybe; - actor?: Maybe; - app?: Maybe; - createdAt: Scalars['DateTime']; - event: NotificationEvent; - id: Scalars['ID']; - type: NotificationType; -}; - -export type NotificationSubscriptionFilter = { - accountId?: InputMaybe; - appId?: InputMaybe; - event?: InputMaybe; - type?: InputMaybe; -}; - -export type NotificationSubscriptionMutation = { - __typename?: 'NotificationSubscriptionMutation'; - subscribeToEventForAccount: SubscribeToNotificationResult; - subscribeToEventForApp: SubscribeToNotificationResult; - unsubscribe: UnsubscribeFromNotificationResult; -}; - - -export type NotificationSubscriptionMutationSubscribeToEventForAccountArgs = { - input: AccountNotificationSubscriptionInput; -}; - - -export type NotificationSubscriptionMutationSubscribeToEventForAppArgs = { - input: AppNotificationSubscriptionInput; -}; - - -export type NotificationSubscriptionMutationUnsubscribeArgs = { - id: Scalars['ID']; -}; - -export type NotificationThresholdExceeded = { - __typename?: 'NotificationThresholdExceeded'; - count: Scalars['Int']; - limit: Scalars['Int']; - threshold: Scalars['Int']; - type: BuildLimitThresholdExceededMetadataType; -}; - -export enum NotificationType { - Email = 'EMAIL', - Web = 'WEB' -} - -export type NotificationsSentOverTimeData = { - __typename?: 'NotificationsSentOverTimeData'; - data: LineChartData; -}; - -export type Offer = { - __typename?: 'Offer'; - features?: Maybe>>; - id: Scalars['ID']; - prerequisite?: Maybe; - price: Scalars['Int']; - quantity?: Maybe; - stripeId: Scalars['ID']; - trialLength?: Maybe; - type: OfferType; -}; - -export type OfferPrerequisite = { - __typename?: 'OfferPrerequisite'; - stripeIds: Array; - type: Scalars['String']; -}; - -export enum OfferType { - /** Addon, or supplementary subscription */ - Addon = 'ADDON', - /** Advanced Purchase of Paid Resource */ - Prepaid = 'PREPAID', - /** Term subscription */ - Subscription = 'SUBSCRIPTION' -} - -export enum OnboardingDeviceType { - Device = 'DEVICE', - Simulator = 'SIMULATOR' -} - -export enum OnboardingEnvironment { - DevBuild = 'DEV_BUILD', - ExpoGo = 'EXPO_GO' -} - -export enum Order { - Asc = 'ASC', - Desc = 'DESC' -} - -export type PageInfo = { - __typename?: 'PageInfo'; - endCursor?: Maybe; - hasNextPage: Scalars['Boolean']; - hasPreviousPage: Scalars['Boolean']; - startCursor?: Maybe; -}; - -export type PartialManifest = { - assets: Array>; - extra?: InputMaybe; - launchAsset: PartialManifestAsset; -}; - -export type PartialManifestAsset = { - bundleKey: Scalars['String']; - contentType: Scalars['String']; - fileExtension?: InputMaybe; - fileSHA256: Scalars['String']; - storageKey: Scalars['String']; -}; - -export type PaymentDetails = { - __typename?: 'PaymentDetails'; - address?: Maybe

; - card?: Maybe; - id: Scalars['ID']; -}; - -export enum Permission { - Admin = 'ADMIN', - Own = 'OWN', - Publish = 'PUBLISH', - View = 'VIEW' -} - -export type PlanEnablement = Concurrencies | EasTotalPlanEnablement; - -export type Project = { - description: Scalars['String']; - fullName: Scalars['String']; - /** @deprecated No longer supported */ - iconUrl?: Maybe; - id: Scalars['ID']; - name: Scalars['String']; - published: Scalars['Boolean']; - slug: Scalars['String']; - updated: Scalars['DateTime']; - username: Scalars['String']; -}; - -export type ProjectArchiveSourceInput = { - bucketKey?: InputMaybe; - gitRef?: InputMaybe; - metadataLocation?: InputMaybe; - repositoryUrl?: InputMaybe; - type: ProjectArchiveSourceType; - url?: InputMaybe; -}; - -export enum ProjectArchiveSourceType { - Gcs = 'GCS', - Git = 'GIT', - None = 'NONE', - S3 = 'S3', - Url = 'URL' -} - -export type ProjectPublicData = { - __typename?: 'ProjectPublicData'; - fullName: Scalars['String']; - id: Scalars['ID']; -}; - -export type ProjectQuery = { - __typename?: 'ProjectQuery'; - /** @deprecated See byAccountNameAndSlug */ - byUsernameAndSlug: Project; -}; - - -export type ProjectQueryByUsernameAndSlugArgs = { - platform?: InputMaybe; - sdkVersions?: InputMaybe>>; - slug: Scalars['String']; - username: Scalars['String']; -}; - -export type PublicArtifacts = { - __typename?: 'PublicArtifacts'; - applicationArchiveUrl?: Maybe; - buildUrl?: Maybe; -}; - -export type PublishUpdateGroupInput = { - awaitingCodeSigningInfo?: InputMaybe; - branchId: Scalars['String']; - excludedAssets?: InputMaybe>; - gitCommitHash?: InputMaybe; - isGitWorkingTreeDirty?: InputMaybe; - message?: InputMaybe; - rollBackToEmbeddedInfoGroup?: InputMaybe; - rolloutInfoGroup?: InputMaybe; - runtimeFingerprintSource?: InputMaybe; - runtimeVersion: Scalars['String']; - turtleJobRunId?: InputMaybe; - updateInfoGroup?: InputMaybe; -}; - -export enum RequestMethod { - Delete = 'DELETE', - Get = 'GET', - Head = 'HEAD', - Options = 'OPTIONS', - Patch = 'PATCH', - Post = 'POST', - Put = 'PUT' -} - -export type RequestsFilters = { - cacheStatus?: InputMaybe>; - continent?: InputMaybe>; - hasCustomDomainOrigin?: InputMaybe; - isAsset?: InputMaybe; - isCrash?: InputMaybe; - isVerifiedBot?: InputMaybe; - method?: InputMaybe>; - os?: InputMaybe>; - pathname?: InputMaybe; - responseType?: InputMaybe>; - status?: InputMaybe>; - statusType?: InputMaybe>; -}; - -export type RequestsOrderBy = { - direction?: InputMaybe; - field: RequestsOrderByField; -}; - -export enum RequestsOrderByDirection { - Asc = 'ASC', - Desc = 'DESC' -} - -export enum RequestsOrderByField { - AssetsSum = 'ASSETS_SUM', - CacheHitRatio = 'CACHE_HIT_RATIO', - CachePassRatio = 'CACHE_PASS_RATIO', - CrashesSum = 'CRASHES_SUM', - Duration = 'DURATION', - RequestsSum = 'REQUESTS_SUM' -} - -export type RescindUserInvitationResult = { - __typename?: 'RescindUserInvitationResult'; - id: Scalars['ID']; -}; - -export enum ResourceClassExperiment { - C3D = 'C3D', - N2 = 'N2' -} - -export enum ResponseCacheStatus { - Hit = 'HIT', - Miss = 'MISS', - Pass = 'PASS' -} - -export enum ResponseStatusType { - ClientError = 'CLIENT_ERROR', - None = 'NONE', - Redirect = 'REDIRECT', - ServerError = 'SERVER_ERROR', - Successful = 'SUCCESSFUL' -} - -export enum ResponseType { - Asset = 'ASSET', - Crash = 'CRASH', - Rejected = 'REJECTED', - Route = 'ROUTE' -} - -/** Represents a robot (not human) actor. */ -export type Robot = Actor & { - __typename?: 'Robot'; - /** Access Tokens belonging to this actor */ - accessTokens: Array; - /** Associated accounts */ - accounts: Array; - created: Scalars['DateTime']; - displayName: Scalars['String']; - /** Experiments associated with this actor */ - experiments: Array; - /** - * Server feature gate values for this actor, optionally filtering by desired gates. - * Only resolves for the viewer. - */ - featureGates: Scalars['JSONObject']; - firstName?: Maybe; - id: Scalars['ID']; - isExpoAdmin: Scalars['Boolean']; - lastDeletionAttemptTime?: Maybe; -}; - - -/** Represents a robot (not human) actor. */ -export type RobotFeatureGatesArgs = { - filter?: InputMaybe>; -}; - -export type RobotDataInput = { - name?: InputMaybe; -}; - -export type RobotMutation = { - __typename?: 'RobotMutation'; - /** Create a Robot and grant it Permissions on an Account */ - createRobotForAccount: Robot; - /** Schedule deletion of a Robot */ - scheduleRobotDeletion: BackgroundJobReceipt; - /** Update a Robot */ - updateRobot: Robot; -}; - - -export type RobotMutationCreateRobotForAccountArgs = { - accountID: Scalars['String']; - permissions: Array>; - robotData?: InputMaybe; -}; - - -export type RobotMutationScheduleRobotDeletionArgs = { - id: Scalars['ID']; -}; - - -export type RobotMutationUpdateRobotArgs = { - id: Scalars['String']; - robotData: RobotDataInput; -}; - -export enum Role { - Admin = 'ADMIN', - Custom = 'CUSTOM', - Developer = 'DEVELOPER', - HasAdmin = 'HAS_ADMIN', - NotAdmin = 'NOT_ADMIN', - Owner = 'OWNER', - ViewOnly = 'VIEW_ONLY' -} - -export type RootMutation = { - __typename?: 'RootMutation'; - /** - * This is a placeholder field - * @deprecated Not used. - */ - _doNotUse?: Maybe; - /** Mutations that create, read, update, and delete AccessTokens for Actors */ - accessToken: AccessTokenMutation; - /** Mutations that modify an Account */ - account: AccountMutation; - /** Mutations that create, update, and delete an AccountSSOConfiguration */ - accountSSOConfiguration: AccountSsoConfigurationMutation; - /** Mutations for Actor experiments */ - actorExperiment: ActorExperimentMutation; - /** Mutations that modify the build credentials for an Android app */ - androidAppBuildCredentials: AndroidAppBuildCredentialsMutation; - /** Mutations that modify the credentials for an Android app */ - androidAppCredentials: AndroidAppCredentialsMutation; - /** Mutations that modify an FCM credential */ - androidFcm: AndroidFcmMutation; - /** Mutations that modify a Keystore */ - androidKeystore: AndroidKeystoreMutation; - /** Mutations that modify an App */ - app?: Maybe; - /** Mutations that modify an App Store Connect Api Key */ - appStoreConnectApiKey: AppStoreConnectApiKeyMutation; - /** Mutations that modify an AppVersion */ - appVersion: AppVersionMutation; - /** Mutations that modify an Identifier for an iOS App */ - appleAppIdentifier: AppleAppIdentifierMutation; - /** Mutations that modify an Apple Device */ - appleDevice: AppleDeviceMutation; - /** Mutations that modify an Apple Device registration request */ - appleDeviceRegistrationRequest: AppleDeviceRegistrationRequestMutation; - /** Mutations that modify a Distribution Certificate */ - appleDistributionCertificate: AppleDistributionCertificateMutation; - /** Mutations that modify a Provisioning Profile */ - appleProvisioningProfile: AppleProvisioningProfileMutation; - /** Mutations that modify an Apple Push Notification key */ - applePushKey: ApplePushKeyMutation; - /** Mutations that modify an Apple Team */ - appleTeam: AppleTeamMutation; - asset: AssetMutation; - auditLog: AuditLogMutation; - /** Mutations that modify an EAS Build */ - build: BuildMutation; - /** Mutations that create, update, and delete Build Annotations */ - buildAnnotation: BuildAnnotationMutation; - customDomain: CustomDomainMutation; - deployments: DeploymentsMutation; - /** Mutations that assign or modify DevDomainNames for apps */ - devDomainName: AppDevDomainNameMutation; - /** Mutations for Discord users */ - discordUser: DiscordUserMutation; - /** Mutations that modify an EmailSubscription */ - emailSubscription: EmailSubscriptionMutation; - /** Mutations that create and delete EnvironmentSecrets */ - environmentSecret: EnvironmentSecretMutation; - /** Mutations that create and delete EnvironmentVariables */ - environmentVariable: EnvironmentVariableMutation; - /** Mutations that utilize services facilitated by the GitHub App */ - githubApp: GitHubAppMutation; - /** Mutations for GitHub App installations */ - githubAppInstallation: GitHubAppInstallationMutation; - /** Mutations for GitHub build triggers */ - githubBuildTrigger: GitHubBuildTriggerMutation; - githubJobRunTrigger: GitHubJobRunTriggerMutation; - /** Mutations for GitHub repositories */ - githubRepository: GitHubRepositoryMutation; - /** Mutations for GitHub repository settings */ - githubRepositorySettings: GitHubRepositorySettingsMutation; - /** Mutations for GitHub users */ - githubUser: GitHubUserMutation; - /** Mutations that modify a Google Service Account Key */ - googleServiceAccountKey: GoogleServiceAccountKeyMutation; - /** Mutations that modify the build credentials for an iOS app */ - iosAppBuildCredentials: IosAppBuildCredentialsMutation; - /** Mutations that modify the credentials for an iOS app */ - iosAppCredentials: IosAppCredentialsMutation; - /** Mutations that modify an EAS Build */ - jobRun: JobRunMutation; - keystoreGenerationUrl: KeystoreGenerationUrlMutation; - /** Mutations that modify the currently authenticated User */ - me: MeMutation; - /** Mutations that modify a NotificationSubscription */ - notificationSubscription: NotificationSubscriptionMutation; - /** Mutations that create, update, and delete Robots */ - robot: RobotMutation; - serverlessFunction: ServerlessFunctionMutation; - /** Mutations that modify an EAS Submit submission */ - submission: SubmissionMutation; - update: UpdateMutation; - updateBranch: UpdateBranchMutation; - updateChannel: UpdateChannelMutation; - uploadSession: UploadSession; - /** Mutations that create, update, and delete pinned apps */ - userAppPins: UserAppPinMutation; - userAuditLog: UserAuditLogMutation; - /** Mutations that create, delete, and accept UserInvitations */ - userInvitation: UserInvitationMutation; - /** Mutations that create, delete, update Webhooks */ - webhook: WebhookMutation; - /** Mutations that modify a websiteNotification */ - websiteNotifications: WebsiteNotificationMutation; - workflowJob: WorkflowJobMutation; -}; - - -export type RootMutationAccountArgs = { - accountName?: InputMaybe; -}; - - -export type RootMutationAppArgs = { - appId?: InputMaybe; -}; - - -export type RootMutationBuildArgs = { - buildId?: InputMaybe; -}; - -export type RootQuery = { - __typename?: 'RootQuery'; - /** - * This is a placeholder field - * @deprecated Not used. - */ - _doNotUse?: Maybe; - /** Top-level query object for querying Accounts. */ - account: AccountQuery; - /** Top-level query object for querying AccountSSOConfigurationPublicData */ - accountSSOConfigurationPublicData: AccountSsoConfigurationPublicDataQuery; - /** - * Top-level query object for querying Actors. - * @deprecated Public actor queries are no longer supported - */ - actor: ActorQuery; - /** - * Public apps in the app directory - * @deprecated Use 'all' field under 'app'. - */ - allPublicApps?: Maybe>>; - app: AppQuery; - /** - * Look up app by app id - * @deprecated Use 'byId' field under 'app'. - */ - appByAppId?: Maybe; - /** Top-level query object for querying App Store Connect API Keys. */ - appStoreConnectApiKey: AppStoreConnectApiKeyQuery; - /** Top-level query object for querying Apple Device registration requests. */ - appleDeviceRegistrationRequest: AppleDeviceRegistrationRequestQuery; - /** Top-level query object for querying Apple Teams. */ - appleTeam: AppleTeamQuery; - asset: AssetQuery; - /** Top-level query object for querying Account Audit Logs. */ - auditLogs: AuditLogQuery; - backgroundJobReceipt: BackgroundJobReceiptQuery; - /** Top-level query object for querying Branchs. */ - branches: BranchQuery; - /** Top-level query object for querying annotations. */ - buildAnnotations: BuildAnnotationsQuery; - /** Top-level query object for querying BuildPublicData publicly. */ - buildPublicData: BuildPublicDataQuery; - builds: BuildQuery; - /** Top-level query object for querying Channels. */ - channels: ChannelQuery; - /** Top-level query object for querying Deployments. */ - deployments: DeploymentQuery; - /** Top-level query object for querying Experimentation configuration. */ - experimentation: ExperimentationQuery; - /** Top-level query object for querying GitHub App information and resources it has access to. */ - githubApp: GitHubAppQuery; - /** Top-level query object for querying Google Service Account Keys. */ - googleServiceAccountKey: GoogleServiceAccountKeyQuery; - /** Top-level query object for querying Stripe Invoices. */ - invoice: InvoiceQuery; - jobRun: JobRunQuery; - /** - * If authenticated as a typical end user, this is the appropriate top-level - * query object - */ - me?: Maybe; - /** - * If authenticated as any type of Actor, this is the appropriate top-level - * query object - */ - meActor?: Maybe; - /** - * If authenticated as any type of human end user (Actor types User or SSOUser), - * this is the appropriate top-level query object - */ - meUserActor?: Maybe; - /** @deprecated Snacks and apps should be queried separately */ - project: ProjectQuery; - /** Top-level query object for querying Runtimes. */ - runtimes: RuntimeQuery; - snack: SnackQuery; - /** Top-level query object for querying Expo status page services. */ - statuspageService: StatuspageServiceQuery; - submissions: SubmissionQuery; - /** Top-level query object for querying Updates. */ - updates: UpdateQuery; - /** fetch all updates in a group */ - updatesByGroup: Array; - /** - * Top-level query object for querying Users. - * @deprecated Public user queries are no longer supported - */ - user: UserQuery; - /** - * Top-level query object for querying UserActors. - * @deprecated Public user queries are no longer supported - */ - userActor: UserActorQuery; - /** Top-level query object for querying UserActorPublicData publicly. */ - userActorPublicData: UserActorPublicDataQuery; - /** Top-level query object for querying User Audit Logs. */ - userAuditLogs: UserAuditLogQuery; - /** @deprecated Use 'byId' field under 'user'. */ - userByUserId?: Maybe; - /** @deprecated Use 'byUsername' field under 'user'. */ - userByUsername?: Maybe; - /** Top-level query object for querying UserInvitationPublicData publicly. */ - userInvitationPublicData: UserInvitationPublicDataQuery; - /** - * If authenticated as a typical end user, this is the appropriate top-level - * query object - */ - viewer?: Maybe; - /** Top-level query object for querying Webhooks. */ - webhook: WebhookQuery; - workerDeployment: WorkerDeploymentQuery; - workflowJobs: WorkflowJobQuery; - workflowRevisions: WorkflowRevisionQuery; - workflowRuns: WorkflowRunQuery; - workflows: WorkflowQuery; -}; - - -export type RootQueryAllPublicAppsArgs = { - filter: AppsFilter; - limit?: InputMaybe; - offset?: InputMaybe; - sort: AppSort; -}; - - -export type RootQueryAppByAppIdArgs = { - appId: Scalars['String']; -}; - - -export type RootQueryUpdatesByGroupArgs = { - group: Scalars['ID']; -}; - - -export type RootQueryUserByUserIdArgs = { - userId: Scalars['String']; -}; - - -export type RootQueryUserByUsernameArgs = { - username: Scalars['String']; -}; - -export type Runtime = { - __typename?: 'Runtime'; - app: App; - builds: AppBuildsConnection; - createdAt: Scalars['DateTime']; - deployments: DeploymentsConnection; - fingerprintDebugInfoUrl?: Maybe; - firstBuildCreatedAt?: Maybe; - id: Scalars['ID']; - updatedAt: Scalars['DateTime']; - updates: AppUpdatesConnection; - version: Scalars['String']; -}; - - -export type RuntimeBuildsArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type RuntimeDeploymentsArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type RuntimeUpdatesArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type RuntimeBuildsFilterInput = { - channel?: InputMaybe; - developmentClient?: InputMaybe; - distributions?: InputMaybe>; - platforms?: InputMaybe>; - releaseChannel?: InputMaybe; - simulator?: InputMaybe; -}; - -export type RuntimeDeploymentsFilterInput = { - channel?: InputMaybe; -}; - -export type RuntimeEdge = { - __typename?: 'RuntimeEdge'; - cursor: Scalars['String']; - node: Runtime; -}; - -export type RuntimeFilterInput = { - /** Only return runtimes shared with this branch */ - branchId?: InputMaybe; -}; - -export type RuntimeQuery = { - __typename?: 'RuntimeQuery'; - /** Query a Runtime by ID */ - byId: Runtime; -}; - - -export type RuntimeQueryByIdArgs = { - runtimeId: Scalars['ID']; -}; - -/** Represents the connection over the runtime edge of an App */ -export type RuntimesConnection = { - __typename?: 'RuntimesConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -/** Represents a human SSO (not robot) actor. */ -export type SsoUser = Actor & UserActor & { - __typename?: 'SSOUser'; - /** Access Tokens belonging to this actor, none at present */ - accessTokens: Array; - accounts: Array; - /** Coalesced project activity for all apps belonging to all accounts this user belongs to. Only resolves for the viewer. */ - activityTimelineProjectActivities: Array; - appCount: Scalars['Int']; - /** @deprecated No longer supported */ - appetizeCode?: Maybe; - /** Apps this user has published. If this user is the viewer, this field returns the apps the user has access to. */ - apps: Array; - bestContactEmail?: Maybe; - created: Scalars['DateTime']; - /** Discord account linked to a user */ - discordUser?: Maybe; - displayName: Scalars['String']; - /** Experiments associated with this actor */ - experiments: Array; - /** - * Server feature gate values for this actor, optionally filtering by desired gates. - * Only resolves for the viewer. - */ - featureGates: Scalars['JSONObject']; - firstName?: Maybe; - fullName?: Maybe; - /** GitHub account linked to a user */ - githubUser?: Maybe; - /** @deprecated No longer supported */ - githubUsername?: Maybe; - id: Scalars['ID']; - /** @deprecated No longer supported */ - industry?: Maybe; - isExpoAdmin: Scalars['Boolean']; - lastDeletionAttemptTime?: Maybe; - lastName?: Maybe; - /** @deprecated No longer supported */ - location?: Maybe; - notificationSubscriptions: Array; - pinnedApps: Array; - preferences: UserPreferences; - /** Associated accounts */ - primaryAccount: Account; - profilePhoto: Scalars['String']; - /** Snacks associated with this account */ - snacks: Array; - /** @deprecated No longer supported */ - twitterUsername?: Maybe; - username: Scalars['String']; - websiteNotificationsPaginated: WebsiteNotificationsConnection; -}; - - -/** Represents a human SSO (not robot) actor. */ -export type SsoUserActivityTimelineProjectActivitiesArgs = { - createdBefore?: InputMaybe; - filterTypes?: InputMaybe>; - limit: Scalars['Int']; -}; - - -/** Represents a human SSO (not robot) actor. */ -export type SsoUserAppsArgs = { - includeUnpublished?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents a human SSO (not robot) actor. */ -export type SsoUserFeatureGatesArgs = { - filter?: InputMaybe>; -}; - - -/** Represents a human SSO (not robot) actor. */ -export type SsoUserNotificationSubscriptionsArgs = { - filter?: InputMaybe; -}; - - -/** Represents a human SSO (not robot) actor. */ -export type SsoUserSnacksArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents a human SSO (not robot) actor. */ -export type SsoUserWebsiteNotificationsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type SsoUserDataInput = { - firstName?: InputMaybe; - lastName?: InputMaybe; -}; - -export type SecondFactorBooleanResult = { - __typename?: 'SecondFactorBooleanResult'; - success: Scalars['Boolean']; -}; - -export type SecondFactorDeviceConfiguration = { - isPrimary: Scalars['Boolean']; - method: SecondFactorMethod; - name: Scalars['String']; - smsPhoneNumber?: InputMaybe; -}; - -export type SecondFactorDeviceConfigurationResult = { - __typename?: 'SecondFactorDeviceConfigurationResult'; - keyURI: Scalars['String']; - secondFactorDevice: UserSecondFactorDevice; - secret: Scalars['String']; -}; - -export type SecondFactorInitiationResult = { - __typename?: 'SecondFactorInitiationResult'; - configurationResults: Array; - plaintextBackupCodes: Array; -}; - -export enum SecondFactorMethod { - /** Google Authenticator (TOTP) */ - Authenticator = 'AUTHENTICATOR', - /** SMS */ - Sms = 'SMS' -} - -export type SecondFactorRegenerateBackupCodesResult = { - __typename?: 'SecondFactorRegenerateBackupCodesResult'; - plaintextBackupCodes: Array; -}; - -export type ServerlessFunctionIdentifierInput = { - gitCommitSHA1: Scalars['String']; -}; - -export type ServerlessFunctionMutation = { - __typename?: 'ServerlessFunctionMutation'; - createDeployment: DeployServerlessFunctionResult; - createUploadPresignedUrl: CreateServerlessFunctionUploadUrlResult; -}; - - -export type ServerlessFunctionMutationCreateDeploymentArgs = { - appId: Scalars['ID']; - serverlessFunctionIdentifierInput: ServerlessFunctionIdentifierInput; -}; - - -export type ServerlessFunctionMutationCreateUploadPresignedUrlArgs = { - appId: Scalars['ID']; - serverlessFunctionIdentifierInput: ServerlessFunctionIdentifierInput; -}; - -export type Snack = Project & { - __typename?: 'Snack'; - /** Description of the Snack */ - description: Scalars['String']; - /** Full name of the Snack, e.g. "@john/mysnack", "@snack/245631" */ - fullName: Scalars['String']; - /** Has the Snack been run without errors */ - hasBeenRunSuccessfully?: Maybe; - hashId: Scalars['String']; - /** @deprecated No longer supported */ - iconUrl?: Maybe; - id: Scalars['ID']; - /** Draft status, which is true when the Snack was not saved explicitly, but auto-saved */ - isDraft: Scalars['Boolean']; - /** Name of the Snack, e.g. "My Snack" */ - name: Scalars['String']; - /** Preview image of the running snack */ - previewImage?: Maybe; - published: Scalars['Boolean']; - /** SDK version of the snack */ - sdkVersion: Scalars['String']; - /** Slug name, e.g. "mysnack", "245631" */ - slug: Scalars['String']; - /** Date and time the Snack was last updated */ - updated: Scalars['DateTime']; - /** Name of the user that created the Snack, or "snack" when the Snack was saved anonymously */ - username: Scalars['String']; -}; - -export type SnackQuery = { - __typename?: 'SnackQuery'; - /** Get snack by hashId */ - byHashId: Snack; - /** - * Get snack by hashId - * @deprecated Use byHashId - */ - byId: Snack; -}; - - -export type SnackQueryByHashIdArgs = { - hashId: Scalars['ID']; -}; - - -export type SnackQueryByIdArgs = { - id: Scalars['ID']; -}; - -export enum StandardOffer { - /** $29 USD per month, 30 day trial */ - Default = 'DEFAULT', - /** $800 USD per month */ - Support = 'SUPPORT', - /** $29 USD per month, 1 year trial */ - YcDeals = 'YC_DEALS', - /** $348 USD per year, 30 day trial */ - YearlySub = 'YEARLY_SUB' -} - -/** Incident for a given component from Expo status page API. */ -export type StatuspageIncident = { - __typename?: 'StatuspageIncident'; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - /** Impact of an incident from Expo status page. */ - impact: StatuspageIncidentImpact; - name: Scalars['String']; - resolvedAt?: Maybe; - /** Shortlink to the incident from Expo status page. */ - shortlink: Scalars['String']; - /** Current status of an incident from Expo status page. */ - status: StatuspageIncidentStatus; - updatedAt: Scalars['DateTime']; - /** List of all updates for an incident from Expo status page. */ - updates: Array; -}; - -/** Possible Incident impact values from Expo status page API. */ -export enum StatuspageIncidentImpact { - Critical = 'CRITICAL', - Maintenance = 'MAINTENANCE', - Major = 'MAJOR', - Minor = 'MINOR', - None = 'NONE' -} - -/** Possible Incident statuses from Expo status page API. */ -export enum StatuspageIncidentStatus { - Completed = 'COMPLETED', - Identified = 'IDENTIFIED', - Investigating = 'INVESTIGATING', - InProgress = 'IN_PROGRESS', - Monitoring = 'MONITORING', - Resolved = 'RESOLVED', - Scheduled = 'SCHEDULED', - Verifying = 'VERIFYING' -} - -/** Update for an Incident from Expo status page API. */ -export type StatuspageIncidentUpdate = { - __typename?: 'StatuspageIncidentUpdate'; - /** Text of an update from Expo status page. */ - body: Scalars['String']; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - /** Status set at the moment of update. */ - status: StatuspageIncidentStatus; -}; - -/** Service monitored by Expo status page. */ -export type StatuspageService = { - __typename?: 'StatuspageService'; - /** Description of a service from Expo status page. */ - description?: Maybe; - id: Scalars['ID']; - /** - * List of last inicdents for a service from Expo status page (we always query for 50 latest incidents for all services) - * sorted by createdAt field in descending order. - */ - incidents: Array; - /** Name of a service monitored by Expo status page. */ - name: StatuspageServiceName; - /** Current status of a service from Expo status page. */ - status: StatuspageServiceStatus; -}; - -/** Name of a service monitored by Expo status page. */ -export enum StatuspageServiceName { - EasBuild = 'EAS_BUILD', - EasSubmit = 'EAS_SUBMIT', - EasUpdate = 'EAS_UPDATE', - GithubApiRequests = 'GITHUB_API_REQUESTS', - GithubWebhooks = 'GITHUB_WEBHOOKS' -} - -export type StatuspageServiceQuery = { - __typename?: 'StatuspageServiceQuery'; - /** Query services from Expo status page by names. */ - byServiceNames: Array; -}; - - -export type StatuspageServiceQueryByServiceNamesArgs = { - serviceNames: Array; -}; - -/** Possible statuses for a service. */ -export enum StatuspageServiceStatus { - DegradedPerformance = 'DEGRADED_PERFORMANCE', - MajorOutage = 'MAJOR_OUTAGE', - Operational = 'OPERATIONAL', - PartialOutage = 'PARTIAL_OUTAGE', - UnderMaintenance = 'UNDER_MAINTENANCE' -} - -export type StripeCoupon = { - __typename?: 'StripeCoupon'; - amountOff?: Maybe; - appliesTo?: Maybe; - id: Scalars['ID']; - name: Scalars['String']; - percentOff?: Maybe; - valid: Scalars['Boolean']; -}; - -export type StripePrice = { - __typename?: 'StripePrice'; - id: Scalars['ID']; -}; - -/** Represents an EAS Submission */ -export type Submission = ActivityTimelineProjectActivity & { - __typename?: 'Submission'; - activityTimestamp: Scalars['DateTime']; - actor?: Maybe; - androidConfig?: Maybe; - app: App; - archiveUrl?: Maybe; - canRetry: Scalars['Boolean']; - cancelingActor?: Maybe; - childSubmission?: Maybe; - completedAt?: Maybe; - createdAt: Scalars['DateTime']; - error?: Maybe; - id: Scalars['ID']; - initiatingActor?: Maybe; - iosConfig?: Maybe; - logFiles: Array; - /** @deprecated Use logFiles instead */ - logsUrl?: Maybe; - /** Retry time starts after completedAt */ - maxRetryTimeMinutes: Scalars['Int']; - parentSubmission?: Maybe; - platform: AppPlatform; - priority?: Maybe; - status: SubmissionStatus; - submittedBuild?: Maybe; - updatedAt: Scalars['DateTime']; -}; - -export enum SubmissionAndroidArchiveType { - Aab = 'AAB', - Apk = 'APK' -} - -export enum SubmissionAndroidReleaseStatus { - Completed = 'COMPLETED', - Draft = 'DRAFT', - Halted = 'HALTED', - InProgress = 'IN_PROGRESS' -} - -export enum SubmissionAndroidTrack { - Alpha = 'ALPHA', - Beta = 'BETA', - Internal = 'INTERNAL', - Production = 'PRODUCTION' -} - -export type SubmissionArchiveSourceInput = { - /** Required if the archive source type is GCS_BUILD_APPLICATION_ARCHIVE or GCS_SUBMIT_ARCHIVE */ - bucketKey?: InputMaybe; - type: SubmissionArchiveSourceType; - /** Required if the archive source type is URL */ - url?: InputMaybe; -}; - -export enum SubmissionArchiveSourceType { - GcsBuildApplicationArchive = 'GCS_BUILD_APPLICATION_ARCHIVE', - GcsSubmitArchive = 'GCS_SUBMIT_ARCHIVE', - Url = 'URL' -} - -export type SubmissionError = { - __typename?: 'SubmissionError'; - errorCode?: Maybe; - message?: Maybe; -}; - -export type SubmissionFilter = { - platform?: InputMaybe; - status?: InputMaybe; -}; - -export type SubmissionMutation = { - __typename?: 'SubmissionMutation'; - /** Cancel an EAS Submit submission */ - cancelSubmission: Submission; - /** Create an Android EAS Submit submission */ - createAndroidSubmission: CreateSubmissionResult; - /** Create an iOS EAS Submit submission */ - createIosSubmission: CreateSubmissionResult; - /** Retry an EAS Submit submission */ - retrySubmission: CreateSubmissionResult; -}; - - -export type SubmissionMutationCancelSubmissionArgs = { - submissionId: Scalars['ID']; -}; - - -export type SubmissionMutationCreateAndroidSubmissionArgs = { - input: CreateAndroidSubmissionInput; -}; - - -export type SubmissionMutationCreateIosSubmissionArgs = { - input: CreateIosSubmissionInput; -}; - - -export type SubmissionMutationRetrySubmissionArgs = { - parentSubmissionId: Scalars['ID']; -}; - -export enum SubmissionPriority { - High = 'HIGH', - Normal = 'NORMAL' -} - -export type SubmissionQuery = { - __typename?: 'SubmissionQuery'; - /** Look up EAS Submission by submission ID */ - byId: Submission; -}; - - -export type SubmissionQueryByIdArgs = { - submissionId: Scalars['ID']; -}; - -export enum SubmissionStatus { - AwaitingBuild = 'AWAITING_BUILD', - Canceled = 'CANCELED', - Errored = 'ERRORED', - Finished = 'FINISHED', - InProgress = 'IN_PROGRESS', - InQueue = 'IN_QUEUE' -} - -export type SubscribeToNotificationResult = { - __typename?: 'SubscribeToNotificationResult'; - notificationSubscription: NotificationSubscription; -}; - -export type SubscriptionDetails = { - __typename?: 'SubscriptionDetails'; - addons: Array; - cancelAt?: Maybe; - concurrencies?: Maybe; - coupon?: Maybe; - endedAt?: Maybe; - futureSubscription?: Maybe; - id: Scalars['ID']; - isDowngrading?: Maybe; - meteredBillingStatus: MeteredBillingStatus; - name?: Maybe; - nextInvoice?: Maybe; - nextInvoiceAmountDueCents?: Maybe; - planEnablement?: Maybe; - planId?: Maybe; - price: Scalars['Int']; - recurringCents?: Maybe; - status?: Maybe; - trialEnd?: Maybe; - upcomingInvoice?: Maybe; - willCancel?: Maybe; -}; - - -export type SubscriptionDetailsPlanEnablementArgs = { - serviceMetric: EasServiceMetric; -}; - -export enum TargetEntityMutationType { - Create = 'CREATE', - Delete = 'DELETE', - Update = 'UPDATE' -} - -export type TestNotificationMetadata = { - __typename?: 'TestNotificationMetadata'; - message: Scalars['String']; -}; - -export type TimelineActivityConnection = { - __typename?: 'TimelineActivityConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type TimelineActivityEdge = { - __typename?: 'TimelineActivityEdge'; - cursor: Scalars['String']; - node: ActivityTimelineProjectActivity; -}; - -export type TimelineActivityFilterInput = { - channels?: InputMaybe>; - platforms?: InputMaybe>; - releaseChannels?: InputMaybe>; - types?: InputMaybe>; -}; - -export type UniqueUsersOverTimeData = { - __typename?: 'UniqueUsersOverTimeData'; - data: LineChartData; -}; - -export type UnsubscribeFromNotificationResult = { - __typename?: 'UnsubscribeFromNotificationResult'; - notificationSubscription: NotificationSubscription; -}; - -export type Update = ActivityTimelineProjectActivity & { - __typename?: 'Update'; - activityTimestamp: Scalars['DateTime']; - actor?: Maybe; - app: App; - awaitingCodeSigningInfo: Scalars['Boolean']; - branch: UpdateBranch; - branchId: Scalars['ID']; - codeSigningInfo?: Maybe; - createdAt: Scalars['DateTime']; - deployments: DeploymentResult; - expoGoSDKVersion?: Maybe; - gitCommitHash?: Maybe; - group: Scalars['String']; - id: Scalars['ID']; - /** Update query field */ - insights: UpdateInsights; - isGitWorkingTreeDirty: Scalars['Boolean']; - isRollBackToEmbedded: Scalars['Boolean']; - jobRun?: Maybe; - manifestFragment: Scalars['String']; - manifestPermalink: Scalars['String']; - message?: Maybe; - platform: Scalars['String']; - rolloutControlUpdate?: Maybe; - rolloutPercentage?: Maybe; - runtime: Runtime; - /** @deprecated Use 'runtime' field . */ - runtimeVersion: Scalars['String']; - updatedAt: Scalars['DateTime']; -}; - - -export type UpdateDeploymentsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type UpdateBranch = { - __typename?: 'UpdateBranch'; - app: App; - appId: Scalars['ID']; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - latestActivity: Scalars['DateTime']; - name: Scalars['String']; - runtimes: RuntimesConnection; - updateGroups: Array>; - updatedAt: Scalars['DateTime']; - updates: Array; -}; - - -export type UpdateBranchRuntimesArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type UpdateBranchUpdateGroupsArgs = { - filter?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -export type UpdateBranchUpdatesArgs = { - filter?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - -export type UpdateBranchMutation = { - __typename?: 'UpdateBranchMutation'; - /** Create an EAS branch for an app */ - createUpdateBranchForApp: UpdateBranch; - /** Delete an EAS branch and all of its updates as long as the branch is not being used by any channels */ - deleteUpdateBranch: DeleteUpdateBranchResult; - /** - * Edit an EAS branch. The branch can be specified either by its ID or - * with the combination of (appId, name). - */ - editUpdateBranch: UpdateBranch; - /** Publish an update group to a branch */ - publishUpdateGroups: Array; -}; - - -export type UpdateBranchMutationCreateUpdateBranchForAppArgs = { - appId: Scalars['ID']; - name: Scalars['String']; -}; - - -export type UpdateBranchMutationDeleteUpdateBranchArgs = { - branchId: Scalars['ID']; -}; - - -export type UpdateBranchMutationEditUpdateBranchArgs = { - input: EditUpdateBranchInput; -}; - - -export type UpdateBranchMutationPublishUpdateGroupsArgs = { - publishUpdateGroupsInput: Array; -}; - -export type UpdateChannel = { - __typename?: 'UpdateChannel'; - app: App; - appId: Scalars['ID']; - branchMapping: Scalars['String']; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - isPaused: Scalars['Boolean']; - name: Scalars['String']; - updateBranches: Array; - updatedAt: Scalars['DateTime']; -}; - - -export type UpdateChannelUpdateBranchesArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - -export type UpdateChannelMutation = { - __typename?: 'UpdateChannelMutation'; - /** - * Create an EAS channel for an app. - * - * In order to work with GraphQL formatting, the branchMapping should be a - * stringified JSON supplied to the mutation as a variable. - */ - createUpdateChannelForApp: UpdateChannel; - /** delete an EAS channel that doesn't point to any branches */ - deleteUpdateChannel: DeleteUpdateChannelResult; - /** - * Edit an EAS channel. - * - * In order to work with GraphQL formatting, the branchMapping should be a - * stringified JSON supplied to the mutation as a variable. - */ - editUpdateChannel: UpdateChannel; - /** Pause updates for an EAS channel. */ - pauseUpdateChannel: UpdateChannel; - /** Resume updates for an EAS channel. */ - resumeUpdateChannel: UpdateChannel; -}; - - -export type UpdateChannelMutationCreateUpdateChannelForAppArgs = { - appId: Scalars['ID']; - branchMapping?: InputMaybe; - name: Scalars['String']; -}; - - -export type UpdateChannelMutationDeleteUpdateChannelArgs = { - channelId: Scalars['ID']; -}; - - -export type UpdateChannelMutationEditUpdateChannelArgs = { - branchMapping: Scalars['String']; - channelId: Scalars['ID']; -}; - - -export type UpdateChannelMutationPauseUpdateChannelArgs = { - channelId: Scalars['ID']; -}; - - -export type UpdateChannelMutationResumeUpdateChannelArgs = { - channelId: Scalars['ID']; -}; - -export type UpdateDeploymentEdge = { - __typename?: 'UpdateDeploymentEdge'; - cursor: Scalars['String']; - node: Deployment; -}; - -export type UpdateDeploymentsConnection = { - __typename?: 'UpdateDeploymentsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type UpdateEnvironmentVariableInput = { - environments?: InputMaybe>; - fileName?: InputMaybe; - id: Scalars['ID']; - isGlobal?: InputMaybe; - name?: InputMaybe; - type?: InputMaybe; - value?: InputMaybe; - visibility?: InputMaybe; -}; - -export type UpdateGitHubBuildTriggerInput = { - autoSubmit: Scalars['Boolean']; - buildProfile: Scalars['String']; - environment?: InputMaybe; - executionBehavior: GitHubBuildTriggerExecutionBehavior; - isActive: Scalars['Boolean']; - platform: AppPlatform; - sourcePattern: Scalars['String']; - submitProfile?: InputMaybe; - targetPattern?: InputMaybe; - type: GitHubBuildTriggerType; -}; - -export type UpdateGitHubJobRunTriggerInput = { - isActive: Scalars['Boolean']; - sourcePattern: Scalars['String']; - targetPattern?: InputMaybe; -}; - -export type UpdateGitHubRepositorySettingsInput = { - baseDirectory: Scalars['String']; -}; - -export type UpdateInfoGroup = { - android?: InputMaybe; - ios?: InputMaybe; - web?: InputMaybe; -}; - -export type UpdateInsights = { - __typename?: 'UpdateInsights'; - cumulativeMetrics: CumulativeMetrics; - id: Scalars['ID']; - totalUniqueUsers: Scalars['Int']; -}; - - -export type UpdateInsightsCumulativeMetricsArgs = { - timespan: InsightsTimespan; -}; - - -export type UpdateInsightsTotalUniqueUsersArgs = { - timespan: InsightsTimespan; -}; - -export type UpdateMutation = { - __typename?: 'UpdateMutation'; - /** Delete an EAS update group */ - deleteUpdateGroup: DeleteUpdateGroupResult; - /** Set code signing info for an update */ - setCodeSigningInfo: Update; - /** Set rollout percentage for an update */ - setRolloutPercentage: Update; -}; - - -export type UpdateMutationDeleteUpdateGroupArgs = { - group: Scalars['ID']; -}; - - -export type UpdateMutationSetCodeSigningInfoArgs = { - codeSigningInfo: CodeSigningInfoInput; - updateId: Scalars['ID']; -}; - - -export type UpdateMutationSetRolloutPercentageArgs = { - percentage: Scalars['Int']; - updateId: Scalars['ID']; -}; - -export type UpdateQuery = { - __typename?: 'UpdateQuery'; - /** Query an Update by ID */ - byId: Update; -}; - - -export type UpdateQueryByIdArgs = { - updateId: Scalars['ID']; -}; - -export type UpdateRollBackToEmbeddedGroup = { - android?: InputMaybe; - ios?: InputMaybe; - web?: InputMaybe; -}; - -export type UpdateRolloutInfo = { - rolloutControlUpdateId: Scalars['ID']; - rolloutPercentage: Scalars['Int']; -}; - -export type UpdateRolloutInfoGroup = { - android?: InputMaybe; - ios?: InputMaybe; - web?: InputMaybe; -}; - -export type UpdatesFilter = { - platform?: InputMaybe; - runtimeVersions?: InputMaybe>; - sdkVersions?: InputMaybe>; -}; - -export type UpdatesMetricsData = { - __typename?: 'UpdatesMetricsData'; - failedInstallsDataset: CumulativeUpdatesDataset; - installsDataset: CumulativeUpdatesDataset; - labels: Array; -}; - -export type UploadSession = { - __typename?: 'UploadSession'; - /** Create an Upload Session for a specific account */ - createAccountScopedUploadSession: Scalars['JSONObject']; - /** Create an Upload Session */ - createUploadSession: Scalars['JSONObject']; -}; - - -export type UploadSessionCreateAccountScopedUploadSessionArgs = { - accountID: Scalars['ID']; - type: AccountUploadSessionType; -}; - - -export type UploadSessionCreateUploadSessionArgs = { - type: UploadSessionType; -}; - -export enum UploadSessionType { - EasBuildGcsProjectMetadata = 'EAS_BUILD_GCS_PROJECT_METADATA', - EasBuildGcsProjectSources = 'EAS_BUILD_GCS_PROJECT_SOURCES', - /** @deprecated Use EAS_BUILD_GCS_PROJECT_SOURCES instead. */ - EasBuildProjectSources = 'EAS_BUILD_PROJECT_SOURCES', - /** @deprecated Use EAS_SUBMIT_GCS_APP_ARCHIVE instead. */ - EasSubmitAppArchive = 'EAS_SUBMIT_APP_ARCHIVE', - EasSubmitGcsAppArchive = 'EAS_SUBMIT_GCS_APP_ARCHIVE', - EasUpdateFingerprint = 'EAS_UPDATE_FINGERPRINT' -} - -export type UsageMetricTotal = { - __typename?: 'UsageMetricTotal'; - billingPeriod: BillingPeriod; - id: Scalars['ID']; - overageMetrics: Array; - planMetrics: Array; - /** Total cost of overages, in cents */ - totalCost: Scalars['Float']; -}; - -export enum UsageMetricType { - Bandwidth = 'BANDWIDTH', - Build = 'BUILD', - Minute = 'MINUTE', - Request = 'REQUEST', - Update = 'UPDATE', - User = 'USER' -} - -export enum UsageMetricsGranularity { - Day = 'DAY', - Hour = 'HOUR', - Minute = 'MINUTE', - Total = 'TOTAL' -} - -export type UsageMetricsTimespan = { - end: Scalars['DateTime']; - start: Scalars['DateTime']; -}; - -/** Represents a human (not robot) actor. */ -export type User = Actor & UserActor & { - __typename?: 'User'; - /** Access Tokens belonging to this actor */ - accessTokens: Array; - accounts: Array; - /** Coalesced project activity for all apps belonging to all accounts this user belongs to. Only resolves for the viewer. */ - activityTimelineProjectActivities: Array; - appCount: Scalars['Int']; - /** @deprecated No longer supported */ - appetizeCode?: Maybe; - /** Apps this user has published */ - apps: Array; - bestContactEmail?: Maybe; - created: Scalars['DateTime']; - /** Discord account linked to a user */ - discordUser?: Maybe; - displayName: Scalars['String']; - email: Scalars['String']; - emailVerified: Scalars['Boolean']; - /** Experiments associated with this actor */ - experiments: Array; - /** - * Server feature gate values for this actor, optionally filtering by desired gates. - * Only resolves for the viewer. - */ - featureGates: Scalars['JSONObject']; - firstName?: Maybe; - fullName?: Maybe; - /** GitHub account linked to a user */ - githubUser?: Maybe; - /** @deprecated No longer supported */ - githubUsername?: Maybe; - /** Whether this user has any pending user invitations. Only resolves for the viewer. */ - hasPendingUserInvitations: Scalars['Boolean']; - id: Scalars['ID']; - /** @deprecated No longer supported */ - industry?: Maybe; - isExpoAdmin: Scalars['Boolean']; - /** @deprecated No longer supported */ - isLegacy: Scalars['Boolean']; - isSecondFactorAuthenticationEnabled: Scalars['Boolean']; - lastDeletionAttemptTime?: Maybe; - lastName?: Maybe; - /** @deprecated No longer supported */ - location?: Maybe; - notificationSubscriptions: Array; - /** Pending UserInvitations for this user. Only resolves for the viewer. */ - pendingUserInvitations: Array; - pinnedApps: Array; - preferences: UserPreferences; - /** Associated accounts */ - primaryAccount: Account; - profilePhoto: Scalars['String']; - /** Get all certified second factor authentication methods */ - secondFactorDevices: Array; - /** Snacks associated with this account */ - snacks: Array; - /** @deprecated No longer supported */ - twitterUsername?: Maybe; - username: Scalars['String']; - websiteNotificationsPaginated: WebsiteNotificationsConnection; -}; - - -/** Represents a human (not robot) actor. */ -export type UserActivityTimelineProjectActivitiesArgs = { - createdBefore?: InputMaybe; - filterTypes?: InputMaybe>; - limit: Scalars['Int']; -}; - - -/** Represents a human (not robot) actor. */ -export type UserAppsArgs = { - includeUnpublished?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents a human (not robot) actor. */ -export type UserFeatureGatesArgs = { - filter?: InputMaybe>; -}; - - -/** Represents a human (not robot) actor. */ -export type UserNotificationSubscriptionsArgs = { - filter?: InputMaybe; -}; - - -/** Represents a human (not robot) actor. */ -export type UserSnacksArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** Represents a human (not robot) actor. */ -export type UserWebsiteNotificationsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActor = { - /** Access Tokens belonging to this user actor */ - accessTokens: Array; - accounts: Array; - /** - * Coalesced project activity for all apps belonging to all accounts this user actor belongs to. - * Only resolves for the viewer. - */ - activityTimelineProjectActivities: Array; - appCount: Scalars['Int']; - /** @deprecated No longer supported */ - appetizeCode?: Maybe; - /** Apps this user has published */ - apps: Array; - bestContactEmail?: Maybe; - created: Scalars['DateTime']; - /** Discord account linked to a user */ - discordUser?: Maybe; - /** - * Best-effort human readable name for this human actor for use in user interfaces during action attribution. - * For example, when displaying a sentence indicating that actor X created a build or published an update. - */ - displayName: Scalars['String']; - /** Experiments associated with this actor */ - experiments: Array; - /** - * Server feature gate values for this user actor, optionally filtering by desired gates. - * Only resolves for the viewer. - */ - featureGates: Scalars['JSONObject']; - firstName?: Maybe; - fullName?: Maybe; - /** GitHub account linked to a user */ - githubUser?: Maybe; - /** @deprecated No longer supported */ - githubUsername?: Maybe; - id: Scalars['ID']; - /** @deprecated No longer supported */ - industry?: Maybe; - isExpoAdmin: Scalars['Boolean']; - lastDeletionAttemptTime?: Maybe; - lastName?: Maybe; - /** @deprecated No longer supported */ - location?: Maybe; - notificationSubscriptions: Array; - pinnedApps: Array; - preferences: UserPreferences; - /** Associated accounts */ - primaryAccount: Account; - profilePhoto: Scalars['String']; - /** Snacks associated with this user's personal account */ - snacks: Array; - /** @deprecated No longer supported */ - twitterUsername?: Maybe; - username: Scalars['String']; - websiteNotificationsPaginated: WebsiteNotificationsConnection; -}; - - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorActivityTimelineProjectActivitiesArgs = { - createdBefore?: InputMaybe; - filterTypes?: InputMaybe>; - limit: Scalars['Int']; -}; - - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorAppsArgs = { - includeUnpublished?: InputMaybe; - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorFeatureGatesArgs = { - filter?: InputMaybe>; -}; - - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorNotificationSubscriptionsArgs = { - filter?: InputMaybe; -}; - - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorSnacksArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorWebsiteNotificationsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorPublicData = { - __typename?: 'UserActorPublicData'; - firstName?: Maybe; - id: Scalars['ID']; - lastName?: Maybe; - profilePhoto: Scalars['String']; - /** Snacks associated with this user's personal account */ - snacks: Array; - username: Scalars['String']; -}; - - -/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */ -export type UserActorPublicDataSnacksArgs = { - limit: Scalars['Int']; - offset: Scalars['Int']; -}; - -export type UserActorPublicDataQuery = { - __typename?: 'UserActorPublicDataQuery'; - /** Get UserActorPublicData by username */ - byUsername: UserActorPublicData; -}; - - -export type UserActorPublicDataQueryByUsernameArgs = { - username: Scalars['String']; -}; - -export type UserActorQuery = { - __typename?: 'UserActorQuery'; - /** - * Query a UserActor by ID - * @deprecated Public user actor queries are no longer supported - */ - byId: UserActor; - /** - * Query a UserActor by username - * @deprecated Public user actor queries are no longer supported - */ - byUsername: UserActor; -}; - - -export type UserActorQueryByIdArgs = { - id: Scalars['ID']; -}; - - -export type UserActorQueryByUsernameArgs = { - username: Scalars['String']; -}; - -export enum UserAgentBrowser { - AndroidMobile = 'ANDROID_MOBILE', - Chrome = 'CHROME', - ChromeIos = 'CHROME_IOS', - Edge = 'EDGE', - FacebookMobile = 'FACEBOOK_MOBILE', - Firefox = 'FIREFOX', - FirefoxIos = 'FIREFOX_IOS', - InternetExplorer = 'INTERNET_EXPLORER', - Konqueror = 'KONQUEROR', - Mozilla = 'MOZILLA', - Opera = 'OPERA', - Safari = 'SAFARI', - SafariMobile = 'SAFARI_MOBILE', - SamsungInternet = 'SAMSUNG_INTERNET', - UcBrowser = 'UC_BROWSER' -} - -export enum UserAgentOs { - Android = 'ANDROID', - ChromeOs = 'CHROME_OS', - Ios = 'IOS', - IpadOs = 'IPAD_OS', - Linux = 'LINUX', - MacOs = 'MAC_OS', - Windows = 'WINDOWS' -} - -export type UserAppPinMutation = { - __typename?: 'UserAppPinMutation'; - pinApp: Scalars['ID']; - unpinApp?: Maybe; -}; - - -export type UserAppPinMutationPinAppArgs = { - appId: Scalars['ID']; -}; - - -export type UserAppPinMutationUnpinAppArgs = { - appId: Scalars['ID']; -}; - -export type UserAuditLog = { - __typename?: 'UserAuditLog'; - actor: Actor; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - ip?: Maybe; - metadata?: Maybe; - targetEntityId: Scalars['ID']; - targetEntityMutationType: TargetEntityMutationType; - targetEntityTypeName: UserEntityTypeName; - targetEntityTypePublicName: Scalars['String']; - user: User; - websiteMessage: Scalars['String']; -}; - -export type UserAuditLogConnection = { - __typename?: 'UserAuditLogConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type UserAuditLogEdge = { - __typename?: 'UserAuditLogEdge'; - cursor: Scalars['String']; - node: UserAuditLog; -}; - -export type UserAuditLogExportInput = { - createdAfter: Scalars['String']; - createdBefore: Scalars['String']; - format: AuditLogsExportFormat; - targetEntityMutationType?: InputMaybe>; - targetEntityTypeName?: InputMaybe>; - userId: Scalars['ID']; -}; - -export type UserAuditLogFilterInput = { - entityTypes?: InputMaybe>; - mutationTypes?: InputMaybe>; -}; - -export type UserAuditLogMutation = { - __typename?: 'UserAuditLogMutation'; - /** Exports User Audit Logs for an user. Returns the ID of the background job receipt. Use BackgroundJobReceiptQuery to get the status of the job. */ - exportUserAuditLogs: BackgroundJobReceipt; -}; - - -export type UserAuditLogMutationExportUserAuditLogsArgs = { - exportInput: UserAuditLogExportInput; -}; - -export type UserAuditLogQuery = { - __typename?: 'UserAuditLogQuery'; - /** Audit logs for user */ - byId: UserAuditLog; - byUserIdPaginated: UserAuditLogConnection; - typeNamesMap: Array; -}; - - -export type UserAuditLogQueryByIdArgs = { - auditLogId: Scalars['ID']; -}; - - -export type UserAuditLogQueryByUserIdPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - filter?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; - userId: Scalars['ID']; -}; - -export type UserDataInput = { - email?: InputMaybe; - firstName?: InputMaybe; - fullName?: InputMaybe; - id?: InputMaybe; - lastName?: InputMaybe; - profilePhoto?: InputMaybe; - username?: InputMaybe; -}; - -export enum UserEntityTypeName { - AccessTokenEntity = 'AccessTokenEntity', - DiscordUserEntity = 'DiscordUserEntity', - GitHubUserEntity = 'GitHubUserEntity', - PasswordEntity = 'PasswordEntity', - SsoUserEntity = 'SSOUserEntity', - UserEntity = 'UserEntity', - UserPermissionEntity = 'UserPermissionEntity', - UserSecondFactorBackupCodesEntity = 'UserSecondFactorBackupCodesEntity', - UserSecondFactorDeviceEntity = 'UserSecondFactorDeviceEntity' -} - -/** An pending invitation sent to an email granting membership on an Account. */ -export type UserInvitation = { - __typename?: 'UserInvitation'; - accountName: Scalars['String']; - /** If the invite is for a personal team, the profile photo of account owner */ - accountProfilePhoto?: Maybe; - created: Scalars['DateTime']; - /** Email to which this invitation was sent */ - email: Scalars['String']; - expires: Scalars['DateTime']; - id: Scalars['ID']; - /** If the invite is for an organization or a personal team */ - isForOrganization: Scalars['Boolean']; - /** Account permissions to be granted upon acceptance of this invitation */ - permissions: Array; - /** Role to be granted upon acceptance of this invitation */ - role: Role; -}; - -export type UserInvitationMutation = { - __typename?: 'UserInvitationMutation'; - /** Accept UserInvitation by ID. Viewer must have matching email and email must be verified. */ - acceptUserInvitationAsViewer: AcceptUserInvitationResult; - /** - * Accept UserInvitation by token. Note that the viewer's email is not required to match - * the email on the invitation. If viewer's email does match that of the invitation, - * their email will also be verified. - */ - acceptUserInvitationByTokenAsViewer: AcceptUserInvitationResult; - /** - * Create a UserInvitation for an email that when accepted grants - * the specified permissions on an Account - */ - createUserInvitationForAccount: UserInvitation; - /** Rescind UserInvitation by ID */ - deleteUserInvitation: RescindUserInvitationResult; - /** - * Delete UserInvitation by token. Note that the viewer's email is not required to match - * the email on the invitation. - */ - deleteUserInvitationByToken: RescindUserInvitationResult; - /** Re-send UserInivitation by ID */ - resendUserInvitation: UserInvitation; -}; - - -export type UserInvitationMutationAcceptUserInvitationAsViewerArgs = { - id: Scalars['ID']; -}; - - -export type UserInvitationMutationAcceptUserInvitationByTokenAsViewerArgs = { - token: Scalars['ID']; -}; - - -export type UserInvitationMutationCreateUserInvitationForAccountArgs = { - accountID: Scalars['ID']; - email: Scalars['String']; - permissions: Array>; -}; - - -export type UserInvitationMutationDeleteUserInvitationArgs = { - id: Scalars['ID']; -}; - - -export type UserInvitationMutationDeleteUserInvitationByTokenArgs = { - token: Scalars['ID']; -}; - - -export type UserInvitationMutationResendUserInvitationArgs = { - id: Scalars['ID']; -}; - -/** Publicly visible data for a UserInvitation. */ -export type UserInvitationPublicData = { - __typename?: 'UserInvitationPublicData'; - accountName: Scalars['String']; - accountProfilePhoto?: Maybe; - created: Scalars['DateTime']; - email: Scalars['String']; - expires: Scalars['DateTime']; - /** Email to which this invitation was sent */ - id: Scalars['ID']; - isForOrganization: Scalars['Boolean']; -}; - -export type UserInvitationPublicDataQuery = { - __typename?: 'UserInvitationPublicDataQuery'; - /** Get UserInvitationPublicData by token */ - byToken: UserInvitationPublicData; -}; - - -export type UserInvitationPublicDataQueryByTokenArgs = { - token: Scalars['ID']; -}; - -export type UserLogNameTypeMapping = { - __typename?: 'UserLogNameTypeMapping'; - publicName: Scalars['String']; - typeName: UserEntityTypeName; -}; - -export type UserPermission = { - __typename?: 'UserPermission'; - actor: Actor; - permissions: Array; - role: Role; - /** @deprecated User type is deprecated */ - user?: Maybe; - userActor?: Maybe; -}; - -export type UserPreferences = { - __typename?: 'UserPreferences'; - onboarding?: Maybe; - selectedAccountName?: Maybe; -}; - -export type UserPreferencesInput = { - onboarding?: InputMaybe; - selectedAccountName?: InputMaybe; -}; - -/** - * Set by website. Used by CLI to continue onboarding process on user's machine - clone repository, - * install dependencies etc. - */ -export type UserPreferencesOnboarding = { - __typename?: 'UserPreferencesOnboarding'; - appId: Scalars['ID']; - /** Can be null if the user has not selected one yet. */ - deviceType?: Maybe; - /** Can be null if the user has not selected one yet. */ - environment?: Maybe; - /** - * Set by CLI when the user has completed that phase. Used by the website to determine when - * the next step can be shown. - */ - isCLIDone?: Maybe; - /** The last time when this object was updated. */ - lastUsed: Scalars['String']; - /** User selects a platform for which they want to build the app. CLI uses this information to start the build. */ - platform?: Maybe; -}; - -export type UserPreferencesOnboardingInput = { - appId: Scalars['ID']; - deviceType?: InputMaybe; - environment?: InputMaybe; - isCLIDone?: InputMaybe; - lastUsed: Scalars['String']; - platform?: InputMaybe; -}; - -export type UserQuery = { - __typename?: 'UserQuery'; - /** - * Query a User by ID - * @deprecated Public user queries are no longer supported - */ - byId: User; - /** - * Query a User by username - * @deprecated Public user queries are no longer supported - */ - byUsername: User; -}; - - -export type UserQueryByIdArgs = { - userId: Scalars['ID']; -}; - - -export type UserQueryByUsernameArgs = { - username: Scalars['String']; -}; - -/** A second factor device belonging to a User */ -export type UserSecondFactorDevice = { - __typename?: 'UserSecondFactorDevice'; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - isCertified: Scalars['Boolean']; - isPrimary: Scalars['Boolean']; - method: SecondFactorMethod; - name: Scalars['String']; - smsPhoneNumber?: Maybe; - updatedAt: Scalars['DateTime']; - user: User; -}; - -export type WebNotificationUpdateReadStateInput = { - id: Scalars['ID']; - isRead: Scalars['Boolean']; -}; - -export type Webhook = { - __typename?: 'Webhook'; - appId: Scalars['ID']; - createdAt: Scalars['DateTime']; - event: WebhookType; - id: Scalars['ID']; - updatedAt: Scalars['DateTime']; - url: Scalars['String']; -}; - -export type WebhookFilter = { - event?: InputMaybe; -}; - -export type WebhookInput = { - event: WebhookType; - secret: Scalars['String']; - url: Scalars['String']; -}; - -export type WebhookMutation = { - __typename?: 'WebhookMutation'; - /** Create a Webhook */ - createWebhook: Webhook; - /** Delete a Webhook */ - deleteWebhook: DeleteWebhookResult; - /** Update a Webhook */ - updateWebhook: Webhook; -}; - - -export type WebhookMutationCreateWebhookArgs = { - appId: Scalars['String']; - webhookInput: WebhookInput; -}; - - -export type WebhookMutationDeleteWebhookArgs = { - webhookId: Scalars['ID']; -}; - - -export type WebhookMutationUpdateWebhookArgs = { - webhookId: Scalars['ID']; - webhookInput: WebhookInput; -}; - -export type WebhookQuery = { - __typename?: 'WebhookQuery'; - byId: Webhook; -}; - - -export type WebhookQueryByIdArgs = { - id: Scalars['ID']; -}; - -export enum WebhookType { - Build = 'BUILD', - Submit = 'SUBMIT' -} - -export type WebsiteNotificationEdge = { - __typename?: 'WebsiteNotificationEdge'; - cursor: Scalars['String']; - node: Notification; -}; - -export type WebsiteNotificationMutation = { - __typename?: 'WebsiteNotificationMutation'; - updateAllWebsiteNotificationReadStateAsRead: Scalars['Boolean']; - updateNotificationReadState: Notification; -}; - - -export type WebsiteNotificationMutationUpdateNotificationReadStateArgs = { - input: WebNotificationUpdateReadStateInput; -}; - -export type WebsiteNotificationsConnection = { - __typename?: 'WebsiteNotificationsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type WorkerCustomDomain = { - __typename?: 'WorkerCustomDomain'; - alias: WorkerDeploymentAlias; - createdAt: Scalars['DateTime']; - dcvDelegationRecord?: Maybe; - devDomainName: Scalars['DevDomainName']; - dnsRecord: CustomDomainDnsRecord; - hostname: Scalars['String']; - id: Scalars['ID']; - setup?: Maybe; - updatedAt: Scalars['DateTime']; - verificationRecord?: Maybe; -}; - -export type WorkerDeployment = ActivityTimelineProjectActivity & { - __typename?: 'WorkerDeployment'; - activityTimestamp: Scalars['DateTime']; - actor?: Maybe; - aliases?: Maybe>; - crashes?: Maybe; - createdAt: Scalars['DateTime']; - deploymentDomain: Scalars['String']; - deploymentIdentifier: Scalars['WorkerDeploymentIdentifier']; - devDomainName: Scalars['DevDomainName']; - id: Scalars['ID']; - initiatingActor?: Maybe; - logs?: Maybe; - requests?: Maybe; - subdomain: Scalars['String']; - url: Scalars['String']; -}; - - -export type WorkerDeploymentCrashesArgs = { - filters?: InputMaybe; - timespan: DatasetTimespan; -}; - - -export type WorkerDeploymentLogsArgs = { - limit?: InputMaybe; - timespan: LogsTimespan; -}; - - -export type WorkerDeploymentRequestsArgs = { - filters?: InputMaybe; - timespan: DatasetTimespan; -}; - -export type WorkerDeploymentAlias = { - __typename?: 'WorkerDeploymentAlias'; - aliasName?: Maybe; - createdAt: Scalars['DateTime']; - deploymentDomain: Scalars['String']; - devDomainName: Scalars['DevDomainName']; - id: Scalars['ID']; - subdomain: Scalars['String']; - updatedAt: Scalars['DateTime']; - url: Scalars['String']; - workerDeployment: WorkerDeployment; -}; - -export type WorkerDeploymentAliasEdge = { - __typename?: 'WorkerDeploymentAliasEdge'; - cursor: Scalars['String']; - node: WorkerDeploymentAlias; -}; - -export type WorkerDeploymentAliasesConnection = { - __typename?: 'WorkerDeploymentAliasesConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type WorkerDeploymentCrashEdge = { - __typename?: 'WorkerDeploymentCrashEdge'; - logs: Array; - node: WorkerDeploymentCrashNode; - request?: Maybe; -}; - -export type WorkerDeploymentCrashNode = { - __typename?: 'WorkerDeploymentCrashNode'; - crashHash: Scalars['ID']; - crashTimestamp: Scalars['DateTime']; - deploymentIdentifier: Scalars['String']; - firstStackLine?: Maybe; - key: Scalars['ID']; - message: Scalars['String']; - name: Scalars['String']; - requestTimestamp: Scalars['DateTime']; - scriptName: Scalars['String']; - stack?: Maybe>; -}; - -export type WorkerDeploymentCrashes = { - __typename?: 'WorkerDeploymentCrashes'; - byCrashHash: Array; - byName: Array; - interval: Scalars['Int']; - minRowsWithoutLimit: Scalars['Int']; - nodes: Array; - summary: WorkerDeploymentCrashesAggregationNode; - timeseries: Array; -}; - -export type WorkerDeploymentCrashesAggregationNode = { - __typename?: 'WorkerDeploymentCrashesAggregationNode'; - crashesPerMs?: Maybe; - crashesSum: Scalars['Int']; - distinctCrashes: Scalars['Int']; - firstOccurredAt: Scalars['DateTime']; - mostRecentlyOccurredAt: Scalars['DateTime']; - sampleRate?: Maybe; -}; - -export type WorkerDeploymentCrashesHashEdge = { - __typename?: 'WorkerDeploymentCrashesHashEdge'; - crashHash: Scalars['ID']; - node: WorkerDeploymentCrashesAggregationNode; - sample: WorkerDeploymentCrashNode; - timeseries: Array; -}; - -export type WorkerDeploymentCrashesNameEdge = { - __typename?: 'WorkerDeploymentCrashesNameEdge'; - name: Scalars['String']; - node: WorkerDeploymentCrashesAggregationNode; - sample: WorkerDeploymentCrashNode; - timeseries: Array; -}; - -export type WorkerDeploymentCrashesTimeseriesEdge = { - __typename?: 'WorkerDeploymentCrashesTimeseriesEdge'; - node?: Maybe; - timestamp: Scalars['DateTime']; -}; - -export type WorkerDeploymentEdge = { - __typename?: 'WorkerDeploymentEdge'; - cursor: Scalars['String']; - node: WorkerDeployment; -}; - -export enum WorkerDeploymentLogLevel { - Debug = 'DEBUG', - Error = 'ERROR', - Fatal = 'FATAL', - Info = 'INFO', - Log = 'LOG', - Warn = 'WARN' -} - -export type WorkerDeploymentLogNode = { - __typename?: 'WorkerDeploymentLogNode'; - level: WorkerDeploymentLogLevel; - message: Scalars['String']; - timestamp: Scalars['DateTime']; -}; - -export type WorkerDeploymentLogs = { - __typename?: 'WorkerDeploymentLogs'; - minRowsWithoutLimit?: Maybe; - nodes: Array; -}; - -export type WorkerDeploymentQuery = { - __typename?: 'WorkerDeploymentQuery'; - byId: WorkerDeployment; -}; - - -export type WorkerDeploymentQueryByIdArgs = { - id: Scalars['ID']; -}; - -export type WorkerDeploymentRequestEdge = { - __typename?: 'WorkerDeploymentRequestEdge'; - crash?: Maybe; - logs: Array; - node: WorkerDeploymentRequestNode; -}; - -export type WorkerDeploymentRequestNode = { - __typename?: 'WorkerDeploymentRequestNode'; - browserKind?: Maybe; - browserVersion?: Maybe; - cacheStatus?: Maybe; - continent?: Maybe; - country?: Maybe; - deploymentIdentifier: Scalars['String']; - duration: Scalars['Int']; - hasCustomDomainOrigin: Scalars['Boolean']; - isAsset: Scalars['Boolean']; - isCrash: Scalars['Boolean']; - isRejected: Scalars['Boolean']; - isStaleIfError: Scalars['Boolean']; - isStaleWhileRevalidate: Scalars['Boolean']; - isVerifiedBot: Scalars['Boolean']; - key: Scalars['ID']; - method: Scalars['String']; - os?: Maybe; - pathname: Scalars['String']; - region?: Maybe; - requestTimestamp: Scalars['DateTime']; - responseType: ResponseType; - scriptName: Scalars['String']; - search?: Maybe; - status: Scalars['Int']; - statusType?: Maybe; -}; - -export type WorkerDeploymentRequests = { - __typename?: 'WorkerDeploymentRequests'; - byBrowser: Array; - byCacheStatus: Array; - byContinent: Array; - byCountry: Array; - byMethod: Array; - byOS: Array; - byResponseType: Array; - byStatusType: Array; - interval: Scalars['Int']; - minRowsWithoutLimit: Scalars['Int']; - nodes: Array; - summary: WorkerDeploymentRequestsAggregationNode; - timeseries: Array; -}; - - -export type WorkerDeploymentRequestsByBrowserArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - - -export type WorkerDeploymentRequestsByCacheStatusArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - - -export type WorkerDeploymentRequestsByContinentArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - - -export type WorkerDeploymentRequestsByCountryArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - - -export type WorkerDeploymentRequestsByMethodArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - - -export type WorkerDeploymentRequestsByOsArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - - -export type WorkerDeploymentRequestsByResponseTypeArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - - -export type WorkerDeploymentRequestsByStatusTypeArgs = { - limit?: InputMaybe; - orderBy?: InputMaybe; -}; - -export type WorkerDeploymentRequestsAggregationNode = { - __typename?: 'WorkerDeploymentRequestsAggregationNode'; - assetsPerMs?: Maybe; - assetsSum: Scalars['Int']; - cacheHitRatio: Scalars['Float']; - cacheHitRatioP50: Scalars['Float']; - cacheHitRatioP90: Scalars['Float']; - cacheHitRatioP99: Scalars['Float']; - cacheHitsPerMs?: Maybe; - cacheHitsSum: Scalars['Int']; - cachePassRatio: Scalars['Float']; - cachePassRatioP50: Scalars['Float']; - cachePassRatioP90: Scalars['Float']; - cachePassRatioP99: Scalars['Float']; - clientErrorRatio: Scalars['Float']; - clientErrorRatioP50: Scalars['Float']; - clientErrorRatioP90: Scalars['Float']; - clientErrorRatioP99: Scalars['Float']; - crashRatio: Scalars['Float']; - crashRatioP50: Scalars['Float']; - crashRatioP90: Scalars['Float']; - crashRatioP99: Scalars['Float']; - crashesPerMs?: Maybe; - crashesSum: Scalars['Int']; - duration: Scalars['Float']; - durationP50: Scalars['Float']; - durationP90: Scalars['Float']; - durationP99: Scalars['Float']; - requestsPerMs?: Maybe; - requestsSum: Scalars['Int']; - sampleRate?: Maybe; - serverErrorRatio: Scalars['Float']; - serverErrorRatioP50: Scalars['Float']; - serverErrorRatioP90: Scalars['Float']; - serverErrorRatioP99: Scalars['Float']; - staleIfErrorPerMs?: Maybe; - staleIfErrorSum: Scalars['Int']; - staleWhileRevalidatePerMs?: Maybe; - staleWhileRevalidateSum: Scalars['Int']; -}; - -export type WorkerDeploymentRequestsBrowserEdge = { - __typename?: 'WorkerDeploymentRequestsBrowserEdge'; - browser?: Maybe; - node: WorkerDeploymentRequestsAggregationNode; -}; - -export type WorkerDeploymentRequestsCacheStatusEdge = { - __typename?: 'WorkerDeploymentRequestsCacheStatusEdge'; - cacheStatus?: Maybe; - node: WorkerDeploymentRequestsAggregationNode; -}; - -export type WorkerDeploymentRequestsContinentEdge = { - __typename?: 'WorkerDeploymentRequestsContinentEdge'; - continent: ContinentCode; - node: WorkerDeploymentRequestsAggregationNode; -}; - -export type WorkerDeploymentRequestsCountryEdge = { - __typename?: 'WorkerDeploymentRequestsCountryEdge'; - country?: Maybe; - node: WorkerDeploymentRequestsAggregationNode; -}; - -export type WorkerDeploymentRequestsMethodEdge = { - __typename?: 'WorkerDeploymentRequestsMethodEdge'; - method: Scalars['String']; - node: WorkerDeploymentRequestsAggregationNode; -}; - -export type WorkerDeploymentRequestsOperatingSystemEdge = { - __typename?: 'WorkerDeploymentRequestsOperatingSystemEdge'; - node: WorkerDeploymentRequestsAggregationNode; - os?: Maybe; -}; - -export type WorkerDeploymentRequestsResponseTypeEdge = { - __typename?: 'WorkerDeploymentRequestsResponseTypeEdge'; - node: WorkerDeploymentRequestsAggregationNode; - responseType: ResponseType; -}; - -export type WorkerDeploymentRequestsStatusTypeEdge = { - __typename?: 'WorkerDeploymentRequestsStatusTypeEdge'; - node: WorkerDeploymentRequestsAggregationNode; - statusType?: Maybe; -}; - -export type WorkerDeploymentRequestsTimeseriesEdge = { - __typename?: 'WorkerDeploymentRequestsTimeseriesEdge'; - byBrowser: Array; - byCacheStatus: Array; - byContinent: Array; - byCountry: Array; - byMethod: Array; - byOS: Array; - byResponseType: Array; - byStatusType: Array; - node?: Maybe; - timestamp: Scalars['DateTime']; -}; - -export type WorkerDeploymentsConnection = { - __typename?: 'WorkerDeploymentsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export enum WorkerLoggerLevel { - Debug = 'DEBUG', - Error = 'ERROR', - Fatal = 'FATAL', - Info = 'INFO', - Trace = 'TRACE', - Warn = 'WARN' -} - -export type Workflow = { - __typename?: 'Workflow'; - app: App; - createdAt: Scalars['DateTime']; - fileName: Scalars['String']; - id: Scalars['ID']; - name?: Maybe; - revisionsPaginated: WorkflowRevisionsConnection; - runsPaginated: WorkflowRunsConnection; - updatedAt: Scalars['DateTime']; -}; - - -export type WorkflowRevisionsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type WorkflowRunsPaginatedArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type WorkflowJob = { - __typename?: 'WorkflowJob'; - createdAt: Scalars['DateTime']; - credentialsAppleDeviceRegistrationRequest?: Maybe; - errors: Array; - id: Scalars['ID']; - key: Scalars['String']; - name: Scalars['String']; - outputs: Scalars['JSONObject']; - requiredJobKeys: Array; - status: WorkflowJobStatus; - turtleBuild?: Maybe; - turtleJobRun?: Maybe; - turtleSubmission?: Maybe; - type: WorkflowJobType; - updatedAt: Scalars['DateTime']; - workflowRun: WorkflowRun; -}; - -export type WorkflowJobError = { - __typename?: 'WorkflowJobError'; - message: Scalars['String']; - title: Scalars['String']; -}; - -export type WorkflowJobMutation = { - __typename?: 'WorkflowJobMutation'; - approveWorkflowJob: Scalars['ID']; -}; - - -export type WorkflowJobMutationApproveWorkflowJobArgs = { - workflowJobId: Scalars['ID']; -}; - -export type WorkflowJobQuery = { - __typename?: 'WorkflowJobQuery'; - byId: WorkflowJob; -}; - - -export type WorkflowJobQueryByIdArgs = { - workflowJobId: Scalars['ID']; -}; - -export enum WorkflowJobStatus { - ActionRequired = 'ACTION_REQUIRED', - Canceled = 'CANCELED', - Failure = 'FAILURE', - InProgress = 'IN_PROGRESS', - New = 'NEW', - Skipped = 'SKIPPED', - Success = 'SUCCESS' -} - -export enum WorkflowJobType { - AppleDeviceRegistrationRequest = 'APPLE_DEVICE_REGISTRATION_REQUEST', - Build = 'BUILD', - Custom = 'CUSTOM', - MaestroTest = 'MAESTRO_TEST', - RequireApproval = 'REQUIRE_APPROVAL', - Submission = 'SUBMISSION', - Update = 'UPDATE' -} - -/** Look up Workflow by ID */ -export type WorkflowQuery = { - __typename?: 'WorkflowQuery'; - byId: Workflow; -}; - - -/** Look up Workflow by ID */ -export type WorkflowQueryByIdArgs = { - workflowId: Scalars['ID']; -}; - -export type WorkflowRevision = { - __typename?: 'WorkflowRevision'; - blobSha: Scalars['String']; - commitSha: Scalars['String']; - createdAt: Scalars['DateTime']; - id: Scalars['ID']; - workflow: Workflow; - yamlConfig: Scalars['String']; -}; - -export type WorkflowRevisionEdge = { - __typename?: 'WorkflowRevisionEdge'; - cursor: Scalars['String']; - node: WorkflowRevision; -}; - -export type WorkflowRevisionQuery = { - __typename?: 'WorkflowRevisionQuery'; - byId: WorkflowRevision; -}; - - -export type WorkflowRevisionQueryByIdArgs = { - workflowRevisionId: Scalars['ID']; -}; - -export type WorkflowRevisionsConnection = { - __typename?: 'WorkflowRevisionsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type WorkflowRun = { - __typename?: 'WorkflowRun'; - createdAt: Scalars['DateTime']; - gitCommitHash?: Maybe; - gitCommitMessage?: Maybe; - githubRepository?: Maybe; - id: Scalars['ID']; - initiatingUser?: Maybe; - jobs: Array; - name: Scalars['String']; - pullRequestNumber?: Maybe; - requestedGitRef?: Maybe; - status: WorkflowRunStatus; - updatedAt: Scalars['DateTime']; - workflow: Workflow; - workflowRevision?: Maybe; -}; - -export type WorkflowRunEdge = { - __typename?: 'WorkflowRunEdge'; - cursor: Scalars['String']; - node: WorkflowRun; -}; - -export type WorkflowRunQuery = { - __typename?: 'WorkflowRunQuery'; - byId: WorkflowRun; -}; - - -export type WorkflowRunQueryByIdArgs = { - workflowRunId: Scalars['ID']; -}; - -export enum WorkflowRunStatus { - ActionRequired = 'ACTION_REQUIRED', - Canceled = 'CANCELED', - Failure = 'FAILURE', - InProgress = 'IN_PROGRESS', - New = 'NEW', - Success = 'SUCCESS' -} - -export type WorkflowRunsConnection = { - __typename?: 'WorkflowRunsConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type DeleteAndroidAppBuildCredentialsResult = { - __typename?: 'deleteAndroidAppBuildCredentialsResult'; - id: Scalars['ID']; -}; - -export type DeleteAndroidFcmResult = { - __typename?: 'deleteAndroidFcmResult'; - id: Scalars['ID']; -}; - -export type DeleteAppStoreConnectApiKeyResult = { - __typename?: 'deleteAppStoreConnectApiKeyResult'; - id: Scalars['ID']; -}; - -export type DeleteApplePushKeyResult = { - __typename?: 'deleteApplePushKeyResult'; - id: Scalars['ID']; -}; - -export type CommonAppDataFragment = { __typename?: 'App', id: string, name: string, fullName: string, ownerAccount: { __typename?: 'Account', name: string }, firstTwoBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }> }> }; - -export type CommonSnackDataFragment = { __typename?: 'Snack', id: string, name: string, description: string, fullName: string, slug: string, isDraft: boolean, sdkVersion: string }; - -type CurrentUserActorData_SsoUser_Fragment = { __typename: 'SSOUser', id: string, username: string, firstName?: string | null, lastName?: string | null, profilePhoto: string, bestContactEmail?: string | null, accounts: Array<{ __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | { __typename?: 'User', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | null }> }; - -type CurrentUserActorData_User_Fragment = { __typename: 'User', id: string, username: string, firstName?: string | null, lastName?: string | null, profilePhoto: string, bestContactEmail?: string | null, accounts: Array<{ __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | { __typename?: 'User', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | null }> }; - -export type CurrentUserActorDataFragment = CurrentUserActorData_SsoUser_Fragment | CurrentUserActorData_User_Fragment; - -export type UpdateDataFragment = { __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }; - -export type Home_AccountDataQueryVariables = Exact<{ - accountName: Scalars['String']; - appLimit: Scalars['Int']; - snackLimit: Scalars['Int']; - platform: AppPlatform; -}>; - - -export type Home_AccountDataQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, name: string, appCount: number, apps: Array<{ __typename?: 'App', id: string, name: string, fullName: string, ownerAccount: { __typename?: 'Account', name: string }, firstTwoBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }> }> }>, snacks: Array<{ __typename?: 'Snack', id: string, name: string, description: string, fullName: string, slug: string, isDraft: boolean, sdkVersion: string }> } } }; - -export type BranchDetailsQueryVariables = Exact<{ - name: Scalars['String']; - appId: Scalars['String']; - platform: AppPlatform; -}>; - - -export type BranchDetailsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, name: string, slug: string, fullName: string, updateBranchByName?: { __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }> } | null } } }; - -export type BranchesForProjectQueryVariables = Exact<{ - appId: Scalars['String']; - platform: AppPlatform; - limit: Scalars['Int']; - offset: Scalars['Int']; -}>; - - -export type BranchesForProjectQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, name: string, fullName: string, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }> }> } } }; - -export type Home_CurrentUserActorQueryVariables = Exact<{ [key: string]: never; }>; - - -export type Home_CurrentUserActorQuery = { __typename?: 'RootQuery', meUserActor?: { __typename: 'SSOUser', id: string, username: string, firstName?: string | null, lastName?: string | null, profilePhoto: string, bestContactEmail?: string | null, accounts: Array<{ __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | { __typename?: 'User', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | null }> } | { __typename: 'User', id: string, username: string, firstName?: string | null, lastName?: string | null, profilePhoto: string, bestContactEmail?: string | null, accounts: Array<{ __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | { __typename?: 'User', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | null }> } | null }; - -export type Home_AccountAppsQueryVariables = Exact<{ - accountName: Scalars['String']; - limit: Scalars['Int']; - offset: Scalars['Int']; - platform: AppPlatform; -}>; - - -export type Home_AccountAppsQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, appCount: number, apps: Array<{ __typename?: 'App', id: string, name: string, fullName: string, ownerAccount: { __typename?: 'Account', name: string }, firstTwoBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }> }> }> } } }; - -export type ProjectsQueryVariables = Exact<{ - appId: Scalars['String']; - platform: AppPlatform; -}>; - - -export type ProjectsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, name: string, slug: string, fullName: string, ownerAccount: { __typename?: 'Account', name: string }, updateBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }> }> } } }; - -export type Home_AccountSnacksQueryVariables = Exact<{ - accountName: Scalars['String']; - limit: Scalars['Int']; - offset: Scalars['Int']; -}>; - - -export type Home_AccountSnacksQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, name: string, snacks: Array<{ __typename?: 'Snack', id: string, name: string, description: string, fullName: string, slug: string, isDraft: boolean, sdkVersion: string }> } } }; - -export type Home_ViewerPrimaryAccountNameQueryVariables = Exact<{ [key: string]: never; }>; - - -export type Home_ViewerPrimaryAccountNameQuery = { __typename?: 'RootQuery', meUserActor?: { __typename?: 'SSOUser', id: string, primaryAccount: { __typename?: 'Account', id: string, name: string } } | { __typename?: 'User', id: string, primaryAccount: { __typename?: 'Account', id: string, name: string } } | null }; - -export type HomeScreenDataQueryVariables = Exact<{ - accountName: Scalars['String']; - platform: AppPlatform; -}>; - - -export type HomeScreenDataQuery = { __typename?: 'RootQuery', account: { __typename?: 'AccountQuery', byName: { __typename?: 'Account', id: string, name: string, appCount: number, ownerUserActor?: { __typename: 'SSOUser', id: string, username: string, firstName?: string | null, lastName?: string | null, profilePhoto: string, bestContactEmail?: string | null, accounts: Array<{ __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | { __typename?: 'User', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | null }> } | { __typename: 'User', id: string, username: string, firstName?: string | null, lastName?: string | null, profilePhoto: string, bestContactEmail?: string | null, accounts: Array<{ __typename?: 'Account', id: string, name: string, ownerUserActor?: { __typename?: 'SSOUser', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | { __typename?: 'User', id: string, username: string, profilePhoto: string, firstName?: string | null, fullName?: string | null, lastName?: string | null } | null }> } | null, apps: Array<{ __typename?: 'App', id: string, name: string, fullName: string, ownerAccount: { __typename?: 'Account', name: string }, firstTwoBranches: Array<{ __typename?: 'UpdateBranch', id: string, name: string, updates: Array<{ __typename?: 'Update', id: string, group: string, message?: string | null, createdAt: any, runtimeVersion: string, expoGoSDKVersion?: string | null, platform: string, manifestPermalink: string }> }> }>, snacks: Array<{ __typename?: 'Snack', id: string, name: string, description: string, fullName: string, slug: string, isDraft: boolean, sdkVersion: string }> } } }; - -export const UpdateDataFragmentDoc = gql` - fragment UpdateData on Update { - id - group - message - createdAt - runtimeVersion - expoGoSDKVersion - platform - manifestPermalink -} - `; -export const CommonAppDataFragmentDoc = gql` - fragment CommonAppData on App { - id - name - fullName - ownerAccount { - name - } - firstTwoBranches: updateBranches(limit: 2, offset: 0) { - id - name - updates(limit: 1, offset: 0, filter: {platform: $platform}) { - id - ...UpdateData - } - } -} - ${UpdateDataFragmentDoc}`; -export const CommonSnackDataFragmentDoc = gql` - fragment CommonSnackData on Snack { - id - name - description - fullName - slug - isDraft - sdkVersion -} - `; -export const CurrentUserActorDataFragmentDoc = gql` - fragment CurrentUserActorData on UserActor { - __typename - id - username - firstName - lastName - profilePhoto - bestContactEmail - accounts { - id - name - ownerUserActor { - id - username - profilePhoto - firstName - fullName - lastName - } - } -} - `; -export const Home_AccountDataDocument = gql` - query Home_AccountData($accountName: String!, $appLimit: Int!, $snackLimit: Int!, $platform: AppPlatform!) { - account { - byName(accountName: $accountName) { - id - name - appCount - apps(limit: $appLimit, offset: 0, includeUnpublished: true) { - ...CommonAppData - } - snacks(limit: $snackLimit, offset: 0) { - ...CommonSnackData - } - } - } -} - ${CommonAppDataFragmentDoc} -${CommonSnackDataFragmentDoc}`; - -/** - * __useHome_AccountDataQuery__ - * - * To run a query within a React component, call `useHome_AccountDataQuery` and pass it any options that fit your needs. - * When your component renders, `useHome_AccountDataQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useHome_AccountDataQuery({ - * variables: { - * accountName: // value for 'accountName' - * appLimit: // value for 'appLimit' - * snackLimit: // value for 'snackLimit' - * platform: // value for 'platform' - * }, - * }); - */ -export function useHome_AccountDataQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(Home_AccountDataDocument, options); - } -export function useHome_AccountDataLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(Home_AccountDataDocument, options); - } -export type Home_AccountDataQueryHookResult = ReturnType; -export type Home_AccountDataLazyQueryHookResult = ReturnType; -export type Home_AccountDataQueryResult = Apollo.QueryResult; -export function refetchHome_AccountDataQuery(variables: Home_AccountDataQueryVariables) { - return { query: Home_AccountDataDocument, variables: variables } - } -export const BranchDetailsDocument = gql` - query BranchDetails($name: String!, $appId: String!, $platform: AppPlatform!) { - app { - byId(appId: $appId) { - id - name - slug - fullName - updateBranchByName(name: $name) { - id - name - updates(limit: 100, offset: 0, filter: {platform: $platform}) { - id - ...UpdateData - } - } - } - } -} - ${UpdateDataFragmentDoc}`; - -/** - * __useBranchDetailsQuery__ - * - * To run a query within a React component, call `useBranchDetailsQuery` and pass it any options that fit your needs. - * When your component renders, `useBranchDetailsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useBranchDetailsQuery({ - * variables: { - * name: // value for 'name' - * appId: // value for 'appId' - * platform: // value for 'platform' - * }, - * }); - */ -export function useBranchDetailsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(BranchDetailsDocument, options); - } -export function useBranchDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(BranchDetailsDocument, options); - } -export type BranchDetailsQueryHookResult = ReturnType; -export type BranchDetailsLazyQueryHookResult = ReturnType; -export type BranchDetailsQueryResult = Apollo.QueryResult; -export function refetchBranchDetailsQuery(variables: BranchDetailsQueryVariables) { - return { query: BranchDetailsDocument, variables: variables } - } -export const BranchesForProjectDocument = gql` - query BranchesForProject($appId: String!, $platform: AppPlatform!, $limit: Int!, $offset: Int!) { - app { - byId(appId: $appId) { - id - name - fullName - updateBranches(limit: $limit, offset: $offset) { - id - name - updates(limit: 1, offset: 0, filter: {platform: $platform}) { - id - ...UpdateData - } - } - } - } -} - ${UpdateDataFragmentDoc}`; - -/** - * __useBranchesForProjectQuery__ - * - * To run a query within a React component, call `useBranchesForProjectQuery` and pass it any options that fit your needs. - * When your component renders, `useBranchesForProjectQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useBranchesForProjectQuery({ - * variables: { - * appId: // value for 'appId' - * platform: // value for 'platform' - * limit: // value for 'limit' - * offset: // value for 'offset' - * }, - * }); - */ -export function useBranchesForProjectQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(BranchesForProjectDocument, options); - } -export function useBranchesForProjectLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(BranchesForProjectDocument, options); - } -export type BranchesForProjectQueryHookResult = ReturnType; -export type BranchesForProjectLazyQueryHookResult = ReturnType; -export type BranchesForProjectQueryResult = Apollo.QueryResult; -export function refetchBranchesForProjectQuery(variables: BranchesForProjectQueryVariables) { - return { query: BranchesForProjectDocument, variables: variables } - } -export const Home_CurrentUserActorDocument = gql` - query Home_CurrentUserActor { - meUserActor { - ...CurrentUserActorData - } -} - ${CurrentUserActorDataFragmentDoc}`; - -/** - * __useHome_CurrentUserActorQuery__ - * - * To run a query within a React component, call `useHome_CurrentUserActorQuery` and pass it any options that fit your needs. - * When your component renders, `useHome_CurrentUserActorQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useHome_CurrentUserActorQuery({ - * variables: { - * }, - * }); - */ -export function useHome_CurrentUserActorQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(Home_CurrentUserActorDocument, options); - } -export function useHome_CurrentUserActorLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(Home_CurrentUserActorDocument, options); - } -export type Home_CurrentUserActorQueryHookResult = ReturnType; -export type Home_CurrentUserActorLazyQueryHookResult = ReturnType; -export type Home_CurrentUserActorQueryResult = Apollo.QueryResult; -export function refetchHome_CurrentUserActorQuery(variables?: Home_CurrentUserActorQueryVariables) { - return { query: Home_CurrentUserActorDocument, variables: variables } - } -export const Home_AccountAppsDocument = gql` - query Home_AccountApps($accountName: String!, $limit: Int!, $offset: Int!, $platform: AppPlatform!) { - account { - byName(accountName: $accountName) { - id - appCount - apps(limit: $limit, offset: $offset, includeUnpublished: true) { - ...CommonAppData - } - } - } -} - ${CommonAppDataFragmentDoc}`; - -/** - * __useHome_AccountAppsQuery__ - * - * To run a query within a React component, call `useHome_AccountAppsQuery` and pass it any options that fit your needs. - * When your component renders, `useHome_AccountAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useHome_AccountAppsQuery({ - * variables: { - * accountName: // value for 'accountName' - * limit: // value for 'limit' - * offset: // value for 'offset' - * platform: // value for 'platform' - * }, - * }); - */ -export function useHome_AccountAppsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(Home_AccountAppsDocument, options); - } -export function useHome_AccountAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(Home_AccountAppsDocument, options); - } -export type Home_AccountAppsQueryHookResult = ReturnType; -export type Home_AccountAppsLazyQueryHookResult = ReturnType; -export type Home_AccountAppsQueryResult = Apollo.QueryResult; -export function refetchHome_AccountAppsQuery(variables: Home_AccountAppsQueryVariables) { - return { query: Home_AccountAppsDocument, variables: variables } - } -export const ProjectsQueryDocument = gql` - query ProjectsQuery($appId: String!, $platform: AppPlatform!) { - app { - byId(appId: $appId) { - id - name - slug - fullName - ownerAccount { - name - } - updateBranches(limit: 100, offset: 0) { - id - name - updates(limit: 1, offset: 0, filter: {platform: $platform}) { - id - ...UpdateData - } - } - } - } -} - ${UpdateDataFragmentDoc}`; - -/** - * __useProjectsQuery__ - * - * To run a query within a React component, call `useProjectsQuery` and pass it any options that fit your needs. - * When your component renders, `useProjectsQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useProjectsQuery({ - * variables: { - * appId: // value for 'appId' - * platform: // value for 'platform' - * }, - * }); - */ -export function useProjectsQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ProjectsQueryDocument, options); - } -export function useProjectsQueryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ProjectsQueryDocument, options); - } -export type ProjectsQueryHookResult = ReturnType; -export type ProjectsQueryLazyQueryHookResult = ReturnType; -export type ProjectsQueryQueryResult = Apollo.QueryResult; -export function refetchProjectsQuery(variables: ProjectsQueryVariables) { - return { query: ProjectsQueryDocument, variables: variables } - } -export const Home_AccountSnacksDocument = gql` - query Home_AccountSnacks($accountName: String!, $limit: Int!, $offset: Int!) { - account { - byName(accountName: $accountName) { - id - name - snacks(limit: $limit, offset: $offset) { - ...CommonSnackData - } - } - } -} - ${CommonSnackDataFragmentDoc}`; - -/** - * __useHome_AccountSnacksQuery__ - * - * To run a query within a React component, call `useHome_AccountSnacksQuery` and pass it any options that fit your needs. - * When your component renders, `useHome_AccountSnacksQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useHome_AccountSnacksQuery({ - * variables: { - * accountName: // value for 'accountName' - * limit: // value for 'limit' - * offset: // value for 'offset' - * }, - * }); - */ -export function useHome_AccountSnacksQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(Home_AccountSnacksDocument, options); - } -export function useHome_AccountSnacksLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(Home_AccountSnacksDocument, options); - } -export type Home_AccountSnacksQueryHookResult = ReturnType; -export type Home_AccountSnacksLazyQueryHookResult = ReturnType; -export type Home_AccountSnacksQueryResult = Apollo.QueryResult; -export function refetchHome_AccountSnacksQuery(variables: Home_AccountSnacksQueryVariables) { - return { query: Home_AccountSnacksDocument, variables: variables } - } -export const Home_ViewerPrimaryAccountNameDocument = gql` - query Home_ViewerPrimaryAccountName { - meUserActor { - id - primaryAccount { - id - name - } - } -} - `; - -/** - * __useHome_ViewerPrimaryAccountNameQuery__ - * - * To run a query within a React component, call `useHome_ViewerPrimaryAccountNameQuery` and pass it any options that fit your needs. - * When your component renders, `useHome_ViewerPrimaryAccountNameQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useHome_ViewerPrimaryAccountNameQuery({ - * variables: { - * }, - * }); - */ -export function useHome_ViewerPrimaryAccountNameQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(Home_ViewerPrimaryAccountNameDocument, options); - } -export function useHome_ViewerPrimaryAccountNameLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(Home_ViewerPrimaryAccountNameDocument, options); - } -export type Home_ViewerPrimaryAccountNameQueryHookResult = ReturnType; -export type Home_ViewerPrimaryAccountNameLazyQueryHookResult = ReturnType; -export type Home_ViewerPrimaryAccountNameQueryResult = Apollo.QueryResult; -export function refetchHome_ViewerPrimaryAccountNameQuery(variables?: Home_ViewerPrimaryAccountNameQueryVariables) { - return { query: Home_ViewerPrimaryAccountNameDocument, variables: variables } - } -export const HomeScreenDataDocument = gql` - query HomeScreenData($accountName: String!, $platform: AppPlatform!) { - account { - byName(accountName: $accountName) { - id - name - ownerUserActor { - ...CurrentUserActorData - } - apps(limit: 5, offset: 0, includeUnpublished: true) { - ...CommonAppData - } - snacks(limit: 5, offset: 0) { - ...CommonSnackData - } - appCount - } - } -} - ${CurrentUserActorDataFragmentDoc} -${CommonAppDataFragmentDoc} -${CommonSnackDataFragmentDoc}`; - -/** - * __useHomeScreenDataQuery__ - * - * To run a query within a React component, call `useHomeScreenDataQuery` and pass it any options that fit your needs. - * When your component renders, `useHomeScreenDataQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useHomeScreenDataQuery({ - * variables: { - * accountName: // value for 'accountName' - * platform: // value for 'platform' - * }, - * }); - */ -export function useHomeScreenDataQuery(baseOptions: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(HomeScreenDataDocument, options); - } -export function useHomeScreenDataLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(HomeScreenDataDocument, options); - } -export type HomeScreenDataQueryHookResult = ReturnType; -export type HomeScreenDataLazyQueryHookResult = ReturnType; -export type HomeScreenDataQueryResult = Apollo.QueryResult; -export function refetchHomeScreenDataQuery(variables: HomeScreenDataQueryVariables) { - return { query: HomeScreenDataDocument, variables: variables } - } \ No newline at end of file diff --git a/apps/expo-go/src/kernel/Kernel.ts b/apps/expo-go/src/kernel/Kernel.ts deleted file mode 100644 index bbf4e4242ac22a..00000000000000 --- a/apps/expo-go/src/kernel/Kernel.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Linking, NativeModules } from 'react-native'; - -import MockKernel from './MockKernel'; - -const NativeKernel = NativeModules.ExponentKernel || MockKernel; - -export enum ExpoClientReleaseType { - UNKNOWN = 'UNKNOWN', - SIMULATOR = 'SIMULATOR', - ENTERPRISE = 'ENTERPRISE', - DEVELOPMENT = 'DEVELOPMENT', - AD_HOC = 'ADHOC', - APPLE_APP_STORE = 'APPLE_APP_STORE', -} - -export const sdkVersions: string = Array.isArray(NativeKernel.sdkVersions) - ? NativeKernel.sdkVersions.join(',') - : NativeKernel.sdkVersions; - -export const sdkVersionsArray = Array.isArray(NativeKernel.sdkVersions) - ? NativeKernel.sdkVersions - : [NativeKernel.sdkVersions]; - -export const iosClientReleaseType: ExpoClientReleaseType = - NativeKernel.IOSClientReleaseType || ExpoClientReleaseType.UNKNOWN; - -export async function openURLAsync(url: string): Promise { - // ExponentKernel.openURL exists on iOS, and it's the same as Linking.openURL except it will never - // validate whether Expo can open this URL. This addresses cases where, e.g., someone types in a - // http://localhost URL directly into the URL bar. We know they implicitly expect Expo to open - // this, even though it won't validate as an Expo URL. By contrast, Linking.openURL would pass - // such a URL on to the system URL handler. - if (NativeKernel.openURL) { - await NativeKernel.openURL(url); - } else { - await Linking.openURL(url); - } -} - -export function selectQRReader(): void { - NativeKernel.selectQRReader(); -} - -export type KernelSession = { - sessionSecret: string; -}; - -export async function getSessionAsync(): Promise { - return await NativeKernel.getSessionAsync(); -} - -export async function setSessionAsync(session: KernelSession): Promise { - await NativeKernel.setSessionAsync(session); -} - -export async function removeSessionAsync(): Promise { - await NativeKernel.removeSessionAsync(); -} - -export function onEventSuccess(eventId: string, result: object): void { - NativeKernel.onEventSuccess(eventId, result); -} - -export function onEventFailure(eventId: string, message: string): void { - NativeKernel.onEventFailure(eventId, message); -} - -export async function getLastCrashDate(): Promise { - return Number(await NativeKernel.getLastCrashDate()); -} diff --git a/apps/expo-go/src/kernel/MockKernel.ts b/apps/expo-go/src/kernel/MockKernel.ts deleted file mode 100644 index 1d0389ded47330..00000000000000 --- a/apps/expo-go/src/kernel/MockKernel.ts +++ /dev/null @@ -1,70 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; - -const STORAGE_PREFIX = '@@expo@@'; - -/** - * A mock implementation of the native kernel module that can be used when loading Home as a regular - * project or when running it on web. - * - * Longer term, it may make sense to natively define a private, unprivileged implementation of the - * kernel module and use this implementation only on web. - */ -export default { - sdkVersions: '', - sdkVersionsArray: [], - - async getDevMenuSettingsAsync(): Promise { - return null; - }, - - async setDevMenuSettingAsync(_key: string, _value: any): Promise {}, - - async doesCurrentTaskEnableDevtoolsAsync(): Promise { - return false; - }, - - async getDevMenuItemsToShowAsync(): Promise { - return {}; - }, - - async selectDevMenuItemWithKeyAsync(_key: string): Promise {}, - - async reloadAppAsync(): Promise {}, - - async closeDevMenuAsync(): Promise {}, - - async goToHomeAsync(): Promise {}, - - selectQRReader(): void {}, - - async getIsOnboardingFinishedAsync(): Promise { - const item = await AsyncStorage.getItem(`${STORAGE_PREFIX}:onboarding`); - return !!item; - }, - - async setIsOnboardingFinishedAsync(finished: boolean): Promise { - if (finished) { - await AsyncStorage.setItem(`${STORAGE_PREFIX}:onboarding`, '1'); - } else { - await AsyncStorage.removeItem(`${STORAGE_PREFIX}:onboarding`); - } - }, - - async getSessionAsync(): Promise { - const json = await AsyncStorage.getItem(`${STORAGE_PREFIX}:session`); - return json ? JSON.parse(json) : null; - }, - - async setSessionAsync(session: object): Promise { - const json = JSON.stringify(session); - await AsyncStorage.setItem(`${STORAGE_PREFIX}:session`, json); - }, - - async removeSessionAsync(): Promise { - await AsyncStorage.removeItem(`${STORAGE_PREFIX}:session`); - }, - - onEventSuccess(_eventId: string, _result: object): void {}, - - onEventFailure(_eventId: string, _message: string): void {}, -}; diff --git a/apps/expo-go/src/legacy/FriendlyUrls.ts b/apps/expo-go/src/legacy/FriendlyUrls.ts deleted file mode 100644 index 188e671a3d530c..00000000000000 --- a/apps/expo-go/src/legacy/FriendlyUrls.ts +++ /dev/null @@ -1,36 +0,0 @@ -import url from 'url'; - -import Config from '../api/Config'; - -const FriendlyUrls = { - toFriendlyString(rawUrl: string, removeHomeHost: boolean = true) { - if (!rawUrl) { - return ''; - } - - const components = url.parse(rawUrl, false, true); - if (components.hostname === Config.api.host) { - components.slashes = false; - components.protocol = ''; - components.pathname = components.pathname ? components.pathname.substr(1) : ''; - if (removeHomeHost) { - components.host = ''; - components.hostname = ''; - return url.format(components); - } else { - // remove slashes but leave the host alone - return url.format(components).substring(2); - } - } - - const protocol = components.protocol ? components.protocol : ''; - const commonProtocols = ['exp:', 'exps:', 'http:', 'https:']; - if (components.protocol && commonProtocols.includes(components.protocol)) { - // Remove the scheme and slashes - return url.format(components).substr(protocol.length + 2); - } - return rawUrl; - }, -}; - -export default FriendlyUrls; diff --git a/apps/expo-go/src/menu/ClipboardIcon.tsx b/apps/expo-go/src/menu/ClipboardIcon.tsx deleted file mode 100644 index 377a3dd9a2fe61..00000000000000 --- a/apps/expo-go/src/menu/ClipboardIcon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { IconProps } from '@expo/styleguide-native/dist/types'; -import { useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import Svg, { SvgProps, Path } from 'react-native-svg'; - -export function ClipboardIcon(props: Pick & IconProps) { - const { size, color, width, height } = props; - - const theme = useExpoTheme(); - - return ( - - - - - ); -} diff --git a/apps/expo-go/src/menu/DevMenuApp.tsx b/apps/expo-go/src/menu/DevMenuApp.tsx deleted file mode 100644 index 4037c8de775182..00000000000000 --- a/apps/expo-go/src/menu/DevMenuApp.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { ThemeProvider } from '@react-navigation/native'; -import { ThemePreference, ThemeProvider as DCCThemeProvider } from 'expo-dev-client-components'; -import React from 'react'; -import { AppRegistry, useColorScheme } from 'react-native'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; - -import DevMenuBottomSheet from './DevMenuBottomSheet'; -import { DevMenuView } from './DevMenuView'; -import { ColorTheme } from '../constants/Colors'; -import Themes from '../constants/Themes'; -import LocalStorage from '../storage/LocalStorage'; - -function useUserSettings(renderId: string): { preferredAppearance?: string } { - const [settings, setSettings] = React.useState({}); - - React.useEffect(() => { - async function getUserSettings() { - const settings = await LocalStorage.getSettingsAsync(); - setSettings(settings); - } - - getUserSettings(); - }, [renderId]); - - return settings; -} - -function useAppColorScheme(uuid: string): ColorTheme { - const colorScheme = useColorScheme(); - const { preferredAppearance = undefined } = useUserSettings(uuid); - - let theme = preferredAppearance === undefined ? colorScheme : preferredAppearance; - if (theme === undefined) { - theme = 'light'; - } - return theme === 'light' ? ColorTheme.LIGHT : ColorTheme.DARK; -} - -class DevMenuRoot extends React.PureComponent< - { task: { manifestUrl: string; manifestString: string }; uuid: string }, - any -> { - render() { - return ; - } -} - -function DevMenuApp(props: { - task: { manifestUrl: string; manifestString: string }; - uuid: string; -}) { - const theme = useAppColorScheme(props.uuid); - - return ( - - - - - - - - - - - - ); -} - -AppRegistry.registerComponent('HomeMenu', () => DevMenuRoot); diff --git a/apps/expo-go/src/menu/DevMenuBottomSheet.tsx b/apps/expo-go/src/menu/DevMenuBottomSheet.tsx deleted file mode 100644 index c1b394e04a58f4..00000000000000 --- a/apps/expo-go/src/menu/DevMenuBottomSheet.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import BottomSheet, { - BottomSheetScrollView, - useBottomSheetSpringConfigs, -} from '@gorhom/bottom-sheet'; -import React, { useCallback, useEffect, useRef } from 'react'; -import { StyleSheet, View, TouchableWithoutFeedback } from 'react-native'; -import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; - -import DevMenuBottomSheetContext from './DevMenuBottomSheetContext'; -import * as DevMenu from './DevMenuModule'; - -type Props = { - uuid: string; - children?: React.ReactNode; -}; - -function Backdrop({ onPress }: { onPress: () => void }) { - const opacity = useSharedValue(0); - - useEffect(() => { - const timer = setTimeout(() => { - opacity.value = withTiming(0.5, { duration: 350 }); - }, 50); - - return () => clearTimeout(timer); - }, []); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - return ( - - - - ); -} - -function DevMenuBottomSheet({ children, uuid }: Props) { - const bottomSheetRef = useRef(null); - - const onCollapse = useCallback( - () => - new Promise((resolve) => { - bottomSheetRef.current?.close(); - - setTimeout(() => { - resolve(); - DevMenu.closeAsync(); - }, 300); - }), - [] - ); - - const onExpand = useCallback( - () => - new Promise((resolve) => { - bottomSheetRef.current?.expand(); - setTimeout(() => { - resolve(); - }, 300); - }), - [] - ); - - const onChange = useCallback((index: number) => { - if (index === -1) { - DevMenu.closeAsync(); - } - }, []); - - useEffect(() => { - const closeSubscription = DevMenu.listenForCloseRequests(() => { - bottomSheetRef.current?.collapse(); - return new Promise((resolve) => { - resolve(); - }); - }); - return () => { - closeSubscription.remove(); - }; - }, []); - - const onBackdropPress = useCallback(() => { - bottomSheetRef.current?.close(); - }, []); - - const animationConfigs = useBottomSheetSpringConfigs({ - duration: 350, - dampingRatio: 0.8, - overshootClamping: true, - stiffness: 250, - }); - - return ( - - - - - - {children} - - - - - ); -} - -const styles = StyleSheet.create({ - backdrop: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'black', - }, - bottomSheetContainer: { - flex: 1, - }, - contentContainerStyle: {}, - bottomSheetBackground: { - backgroundColor: '#f8f8fa', - }, -}); - -export default DevMenuBottomSheet; diff --git a/apps/expo-go/src/menu/DevMenuBottomSheetContext.ts b/apps/expo-go/src/menu/DevMenuBottomSheetContext.ts deleted file mode 100644 index ff465b71587c99..00000000000000 --- a/apps/expo-go/src/menu/DevMenuBottomSheetContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -export type Context = { - expand: () => Promise; - collapse: () => Promise; -}; - -const DevMenuBottomSheetContext = React.createContext(null); -DevMenuBottomSheetContext.displayName = 'DevMenuBottomSheetContext'; - -export default DevMenuBottomSheetContext; diff --git a/apps/expo-go/src/menu/DevMenuButton/index.tsx b/apps/expo-go/src/menu/DevMenuButton/index.tsx deleted file mode 100644 index 8f107ab3ed7860..00000000000000 --- a/apps/expo-go/src/menu/DevMenuButton/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Spacer, Text, View } from 'expo-dev-client-components'; -import React from 'react'; -import { Platform, TouchableOpacity as TouchableOpacityRN } from 'react-native'; -import { TouchableOpacity as TouchableOpacityGH } from 'react-native-gesture-handler'; - -type Props = { - buttonKey: string; - label: string; - onPress: (key: string) => any; - icon?: React.ReactNode; - isEnabled?: boolean; - detail?: string; -}; - -// When rendered inside bottom sheet, touchables from RN don't work on Android, but the ones from GH don't work on iOS. -const TouchableOpacity = Platform.OS === 'android' ? TouchableOpacityGH : TouchableOpacityRN; - -export function DevMenuButton({ isEnabled = true, buttonKey, label, onPress, icon }: Props) { - function _onPress() { - if (onPress) { - onPress(buttonKey); - } - } - - if (!isEnabled) return null; - - return ( - - - - {icon && isEnabled && ( - <> - {icon} - - - )} - - {label} - - - - - ); -} diff --git a/apps/expo-go/src/menu/DevMenuCloseButton.tsx b/apps/expo-go/src/menu/DevMenuCloseButton.tsx deleted file mode 100644 index 2771bbb95d6c9f..00000000000000 --- a/apps/expo-go/src/menu/DevMenuCloseButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { iconSize, XIcon } from '@expo/styleguide-native'; -import { useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { Platform, TouchableHighlight as TouchableHighlightRN, View } from 'react-native'; -import { TouchableHighlight as TouchableHighlightGH } from 'react-native-gesture-handler'; - -type Props = { - onPress: () => void; -}; - -// When rendered inside bottom sheet, touchables from RN don't work on Android, but the ones from GH don't work on iOS. -const TouchableHighlight = Platform.OS === 'android' ? TouchableHighlightGH : TouchableHighlightRN; - -const HIT_SLOP = { top: 15, bottom: 15, left: 15, right: 15 }; - -export function DevMenuCloseButton({ onPress }: Props) { - const theme = useExpoTheme(); - - return ( - - - - - - ); -} diff --git a/apps/expo-go/src/menu/DevMenuItem/index.tsx b/apps/expo-go/src/menu/DevMenuItem/index.tsx deleted file mode 100644 index 2b8da7c7647194..00000000000000 --- a/apps/expo-go/src/menu/DevMenuItem/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Row, Spacer, Text } from 'expo-dev-client-components'; -import React from 'react'; -import { Platform, TouchableOpacity as TouchableOpacityRN } from 'react-native'; -import { TouchableOpacity as TouchableOpacityGH } from 'react-native-gesture-handler'; - -type Props = { - buttonKey: string; - label: string; - onPress: (key: string) => any; - icon?: React.ReactNode; - isEnabled?: boolean; -}; - -// When rendered inside bottom sheet, touchables from RN don't work on Android, but the ones from GH don't work on iOS. -const TouchableOpacity = Platform.OS === 'android' ? TouchableOpacityGH : TouchableOpacityRN; - -export function DevMenuItem({ isEnabled = true, buttonKey, label, onPress, icon }: Props) { - function _onPress() { - if (onPress) { - onPress(buttonKey); - } - } - - if (!isEnabled) return null; - - return ( - - - {icon && isEnabled && ( - <> - {icon} - - - )} - - {label} - - - - ); -} diff --git a/apps/expo-go/src/menu/DevMenuModule.ts b/apps/expo-go/src/menu/DevMenuModule.ts deleted file mode 100644 index d8cce28dd316c6..00000000000000 --- a/apps/expo-go/src/menu/DevMenuModule.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NativeModules, EventSubscription } from 'react-native'; - -import MockKernel from '../kernel/MockKernel'; -import addListenerWithNativeCallback from '../utils/addListenerWithNativeCallback'; - -const NativeKernel = NativeModules.ExponentKernel || MockKernel; - -export type DevMenuSettings = { - motionGestureEnabled?: boolean; - touchGestureEnabled?: boolean; -}; - -export type DevMenuItem = { - label: string; - isEnabled: boolean; - detail?: string; -}; - -export async function getSettingsAsync(): Promise { - if (!NativeKernel.getDevMenuSettingsAsync) { - return null; - } - return await NativeKernel.getDevMenuSettingsAsync(); -} - -export async function setSettingAsync(key: keyof DevMenuSettings, value?: boolean): Promise { - await NativeKernel.setDevMenuSettingAsync(key, value); -} - -export async function doesCurrentTaskEnableDevtoolsAsync(): Promise { - return await NativeKernel.doesCurrentTaskEnableDevtoolsAsync(); -} - -export async function closeAsync(): Promise { - return await NativeKernel.closeDevMenuAsync(); -} - -export async function getItemsToShowAsync(): Promise<{ [key: string]: DevMenuItem }> { - return await NativeKernel.getDevMenuItemsToShowAsync(); -} - -export async function isOnboardingFinishedAsync(): Promise { - return await NativeKernel.getIsOnboardingFinishedAsync(); -} - -export async function setOnboardingFinishedAsync(finished: boolean): Promise { - await NativeKernel.setIsOnboardingFinishedAsync(finished); -} - -export async function selectItemWithKeyAsync(key: string): Promise { - await NativeKernel.selectDevMenuItemWithKeyAsync(key); -} - -export async function reloadAppAsync(): Promise { - await NativeKernel.reloadAppAsync(); -} - -export async function goToHomeAsync(): Promise { - await NativeKernel.goToHomeAsync(); -} - -export function listenForCloseRequests(listener: (event: any) => Promise): EventSubscription { - return addListenerWithNativeCallback('ExponentKernel.requestToCloseDevMenu', listener); -} diff --git a/apps/expo-go/src/menu/DevMenuOnboarding.tsx b/apps/expo-go/src/menu/DevMenuOnboarding.tsx deleted file mode 100644 index 46ffa90936e88f..00000000000000 --- a/apps/expo-go/src/menu/DevMenuOnboarding.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { - View, - Text, - Button, - Spacer, - scale, - useExpoTheme, - padding, -} from 'expo-dev-client-components'; -import { isDevice } from 'expo-device'; -import React from 'react'; -import { Platform } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { CappedWidthContainerView } from '../components/Views'; - -type Props = { - onClose: () => void; -}; - -const deviceMessage = Platform.select({ - ios: `You can shake your device or long press anywhere on the screen with three fingers to get back to it at any time.`, - android: `You can shake your device to get back to it at any time.`, -}); - -const simulatorMessage = Platform.select({ - ios: ( - - You can open it at any time with the {'\u2318 + d '} keyboard - shortcut{' '} - - ("Connect Hardware Keyboard" must be enabled on your simulator to use this shortcut, you can - toggle it with{' '} - - {'\u2318 + shift + K'} - - ). - - - ), - android: ( - - You can press{' '} - - {'\u2318 + m'} - {' '} - on macOS or{' '} - - Ctrl + m - {' '} - on other platforms to get back to it at any time. - - ), -}); - -export function DevMenuOnboarding({ onClose }: Props) { - const { bottom } = useSafeAreaInsets(); - const theme = useExpoTheme(); - return ( - - - - This is the developer menu. It gives you access to useful tools in Expo Go. - - - - {isDevice ? deviceMessage : simulatorMessage} - - - - - - - - - Continue - - - - - ); -} diff --git a/apps/expo-go/src/menu/DevMenuServerInfo.tsx b/apps/expo-go/src/menu/DevMenuServerInfo.tsx deleted file mode 100644 index 3d67f6af06bc1f..00000000000000 --- a/apps/expo-go/src/menu/DevMenuServerInfo.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Constants from 'expo-constants'; -import { Row, Text, Spacer, useExpoTheme, padding } from 'expo-dev-client-components'; -import React from 'react'; -import { StyleSheet, TouchableOpacity, Clipboard } from 'react-native'; - -import { ClipboardIcon } from './ClipboardIcon'; -import DevIndicator from '../components/DevIndicator'; -import { CappedWidthContainerView } from '../components/Views'; -import FriendlyUrls from '../legacy/FriendlyUrls'; - -type Props = { - task: { manifestUrl: string; manifestString: string }; -}; - -export function DevMenuServerInfo({ task }: Props) { - const manifest = task.manifestString - ? (JSON.parse(task.manifestString) as typeof Constants.manifest | typeof Constants.manifest2) - : null; - const taskUrl = task.manifestUrl ? FriendlyUrls.toFriendlyString(task.manifestUrl) : ''; - const devServerName = - manifest && 'extra' in manifest && manifest.extra?.expoGo?.developer - ? manifest.extra.expoGo.developer.tool - : null; - - async function onCopyTaskUrl() { - const { manifestUrl } = task; - - Clipboard.setString(manifestUrl); - alert(`Copied "${manifestUrl}" to the clipboard.`); - } - const theme = useExpoTheme(); - - return ( - - {devServerName ? ( - <> - - Connected to {devServerName} - - - - ) : null} - - - - {devServerName ? ( - - ) : null} - - {taskUrl} - - - - - - - ); -} - -const styles = StyleSheet.create({ - taskDevServerIndicator: { - marginRight: 8, - }, -}); diff --git a/apps/expo-go/src/menu/DevMenuTaskInfo.tsx b/apps/expo-go/src/menu/DevMenuTaskInfo.tsx deleted file mode 100644 index 03c929a3625a5c..00000000000000 --- a/apps/expo-go/src/menu/DevMenuTaskInfo.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import Ionicons from '@expo/vector-icons/build/Ionicons'; -import Constants from 'expo-constants'; -import { Row, View, Text, useExpoTheme } from 'expo-dev-client-components'; -import React from 'react'; -import { Image, Linking, StyleSheet, TouchableOpacity } from 'react-native'; - -import { CappedWidthContainerView } from '../components/Views'; - -type Props = { - task: { manifestUrl: string; manifestString: string }; -}; - -function stringOrUndefined(anything: T): string | undefined { - if (typeof anything === 'string') { - return anything; - } - - return undefined; -} - -function getInfoFromManifest( - manifest: NonNullable -): { - iconUrl?: string; - taskName?: string; - sdkVersion?: string; - runtimeVersion?: string; - isVerified?: boolean; -} { - if ('metadata' in manifest) { - // modern manifest - return { - // @ts-expect-error iconUrl exists only for local development - iconUrl: manifest?.extra?.expoClient?.iconUrl, - taskName: manifest.extra?.expoClient?.name, - sdkVersion: manifest.extra?.expoClient?.sdkVersion, - runtimeVersion: stringOrUndefined(manifest.runtimeVersion), - isVerified: (manifest as any).isVerified, - }; - } else { - // no properties for bare manifests - return { - iconUrl: undefined, - taskName: undefined, - sdkVersion: undefined, - runtimeVersion: undefined, - isVerified: undefined, - }; - } -} - -export function DevMenuTaskInfo({ task }: Props) { - const theme = useExpoTheme(); - - const manifest = task.manifestString - ? (JSON.parse(task.manifestString) as typeof Constants.manifest | typeof Constants.manifest2) - : null; - const manifestInfo = manifest ? getInfoFromManifest(manifest) : null; - - return ( - - - {manifestInfo?.iconUrl ? ( - - ) : null} - - - {manifestInfo?.taskName ? manifestInfo.taskName : 'Untitled Experience'} - - {manifestInfo?.sdkVersion && ( - - SDK version:{' '} - - {manifestInfo.sdkVersion} - - - )} - {manifestInfo?.runtimeVersion && ( - - Runtime version:{' '} - - {manifestInfo.runtimeVersion} - - - )} - {!manifestInfo?.isVerified && ( - Linking.openURL('https://expo.fyi/unverified-app-expo-go')}> - - - - Unverified - - - - )} - - - - ); -} - -const styles = StyleSheet.create({ - taskIcon: { - width: 40, - height: 40, - marginRight: 8, - borderRadius: 8, - alignSelf: 'center', - backgroundColor: 'transparent', - }, -}); diff --git a/apps/expo-go/src/menu/DevMenuView.tsx b/apps/expo-go/src/menu/DevMenuView.tsx deleted file mode 100644 index 5733ac20d77424..00000000000000 --- a/apps/expo-go/src/menu/DevMenuView.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { HomeFilledIcon, iconSize, RefreshIcon } from '@expo/styleguide-native'; -import MaterialCommunityIcons from '@expo/vector-icons/build/MaterialCommunityIcons'; -import { Divider, useExpoTheme, View } from 'expo-dev-client-components'; -import * as Font from 'expo-font'; -import React, { Fragment, useContext, useEffect, useRef } from 'react'; -import { Image } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { UpgradeWarning } from 'src/screens/HomeScreen/UpgradeWarning'; - -import DevMenuBottomSheetContext from './DevMenuBottomSheetContext'; -import { DevMenuCloseButton } from './DevMenuCloseButton'; -import { DevMenuItem } from './DevMenuItem'; -import * as DevMenu from './DevMenuModule'; -import { DevMenuOnboarding } from './DevMenuOnboarding'; -import { DevMenuServerInfo } from './DevMenuServerInfo'; -import { DevMenuTaskInfo } from './DevMenuTaskInfo'; -import { CappedWidthContainerView } from '../components/Views'; -type Props = { - task: { manifestUrl: string; manifestString: string }; - uuid: string; -}; - -// Base64 icon of the FAB, since the one from `assets` has issues loading. -const base64FabIcon = { - uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAfTSURBVHgB7VxdbttGEJ6hg0ZJC1Q5QdQT1D5BlBPEeYvdwKIfiih9kXMC2yew81Ib7YOpIrCN9CHuCcycwMoJwp6gDhDEclBxMkNSCrlckhL/5AD6ANnSiuSKH2dnZmdmF2CBVCDMCU/MbssAaBPhzwjUJIAWeC+8BKBL/z84/AvfEtDg1Dp0YA6olaBVc6t51x32CMEEn5AZgAMDR/0RGGd1klULQZ60EGyzlJhQAvhHWy7Cbh1EVUpQSGJ2oAIgwc7xX4e7UCEqI0ikhm/gHKYZSsi6hvUNerqHNRBgE/lFRMuQDYcfwMOqpKkSgtbM56tI7pHcKGg7pUtC7POQOxtCY3Bm7V/qjhMJbMBwGWVoEjyARLJZoSNsnlgHZ1AySidobeN5h2XfSvhanvYuk3KWREoaWCpNlsptSCLKQPPk6KAPJaJUgkRygOgN6Ht6eQWNnTzEqGCidgKiNP3g4zIlqTSCfEtFF+qwkuHk8o9mHWFDieD+2kzSEcSkCXn40kpZOsmAkiAKWaNzHCZnpWxyBHJNUc7SR/QbaspvEf0FJaAUgp5sdHV6oVLrIpBrSx9j6xdC6y4Mt6AEFB5igTl/r7YH5NhQA4Lhdh5tLWeoFZYgQ6Ms0fdybagJXl9sBKKt1DSSFPkMKESQSI9m+uB8gsY+1IzbbCHVocY6cbWoLroFBeDNxpU28XNy+jitW4Eec8G4PLZ+H8xyvsV98jVeRs0/NQNdtAM5UYggIugoTQ6LuwVT4inrjhHfED/5Zb5WczT5xoW1Tlfe2Dxc+8fKNdfN35YNcJuvlGHcYMn9DFe9sDUl3wPPjdxK2mTRvabhf5GLIVh8M5tZ566bvzIhS3v8tg3TYWIRmbjz8Xm6yeqa2d3nsdULt11h415eBzW3DrqG67baRuD+k3Xe+sazHpNzAdOTI/As5XqnexQ+T6IEMjSjh6KtnnwHPrUhJ3ITJMNCbbuCu3baOR45iLkV+DTxpNtw21bbEIxpogJa5CaINC5+mhh7Fi+JHAQ20+4LGUbBaxORcs2nLP83OErzfciJ/EqalE6RUq1O3JHzILplM8FnsmaKKYX7EicRvtpXF+Ae5ERpc7E0iKcLyVMRO+k8MftixWBGkOIPsSX6EXKikJmfFuzRdlR/iU3ei1PrwEk6R6wVuwBtmDNqkSB+pquRj6xz0mI2gcS14QagcoLEX4qHQTDVHeDUztQ+C0cnCwfg0lA5QUMYxuZCmvBEBH/LNCM2+YwDc05rZkFNQ2x2nFiHW+IBsyL/SfeS79hr34GKUbmS5vnR5TXLURissKdy3ALpqFRCslC5BOkcNyQqNIGsE/UMMYSIUmalvcwZkFJColWjFj9IEoTsrPWUxm0OW9hZcR9/5n/Li/FwCPVF3VUetUhQ4C3b0VaS1PJ5miTJ5BbIOPf9KOJsLV6Ula2YFrVIkEDmXPG8GTVZkvbYa+6hFxxz30mrS0v3EVwz7j9R8wdORUOM7OpQG0EyNHi4PESWCE3+zIttE40FWqaa2lie85Fz+VAjavWDjq0/2QF0Ncm+qeAgjh5X7RiqqN1RFJK8jOgMs3QJ5bJjuOIRXDNqG2JhBJbI9IoQALY4fvNArQVCxIELJO6BdTKn+kTBXAgaIyBqYsWCeqDm6RwJUTFXglTchKmFihs7Wb0pWBCUgQVBGVgQlIEFQRlYEJSBBUEZqM0PklIXAnfZhSUnKeUjx3AWtM1PzX6lSSh6SxtguMoz/WZdi1rqyaxudLc5CXjukrEnddQc3nivxnWkckOOIQ6kyf9fOs/2ItfgNPQdGl7w90dyHan2qCMqWTlBXn49vpglUoW6bnZNtXLDBdx66icQPRi6CntuqzqAVoAgjOa/iZJ+aEvX6EYzG9pjRhy7Hr8n7TFSYvdZe25ZyE8Q0ofwx6SFK5AQ+8FQO+umhPnXyAmd8E53xCf4zolfW4kM5Is/eShQQBXrtKUT96DYW13T5bBE7I8/nFh/7HPIY6B00Of2iTIPjncih2gyq/pUN/wLOVGggApjwaukUrdTyYAiPpYbkkIpCX6pFui4f7DiF07JMbIg5dBUruF8La6CXXmvy6wmlAbmDrTlNvNS6qZmTJnvNv/RmvDAtKdWjWVVyAakph7DuvCRWmqTVRqYhtwSFGRM7UgjYafutIwKUstmEOwicexCZp7F/K3S0ixrEUkeiLsAirVjwgotsCtEkBRux8r/CXvzkCLpk/RrM2wogEIEyTAjVIuhqPk9XRVeRDIrGu5QUtutcJtkQ+a+2odFOLaIRPWCq4b0pfHWxRktvGS8MEHyhFzEWDXYiPBNvAq+fEgfI39pZgRY0sYDpczFGv7yJyfa6i+NrJKklDpqp6zqs1II8nWRfmmk3EAVw02uKcUQoOodb02+t5a1FNS2LLysbSTEWt2hazYCpHUnRuyFv76Jy8LHCBb/HyV87YhuOJ5hTdkY42AZpWwsECxrsKBEzGVrCvD26QDb4Anpx4ytKaQeiBX+I329kA8ZVv+jsfn6W9iaYozZFqLIxHeyqRJ4hVWTDZcy8e1tbhJG6jYSBRHsA7JfZc1Q5QQJPGmShbXxNa65IB6yOIHf/AZLKnyiXM5KLHWm3BvoK9ALsvWrlph4t3OCT5bEnKnNSvb+ZFMlX/+IEhdCPvD/AUuLfZNqhhYI4Qunu5winQrSCAAAAABJRU5ErkJggg==', -}; - -// These are defined in EXVersionManager.m in a dictionary, ordering needs to be -// done here. -const DEV_MENU_ORDER = [ - 'dev-perf-monitor', - 'dev-inspector', - 'dev-remote-debug', - 'dev-live-reload', - 'dev-hmr', - 'dev-reload', -]; - -function ThemedCustomIcon({ source }: { source: number | { uri: string } }) { - const theme = useExpoTheme(); - return ( - - ); -} - -function ThemedMaterialIcon({ - name, -}: { - name: React.ComponentProps['name']; -}) { - const theme = useExpoTheme(); - return ; -} - -const MENU_ITEMS_ICON_MAPPINGS: { - [key: string]: React.ReactNode; -} = { - 'dev-hmr': , - 'dev-remote-debug': , - 'dev-perf-monitor': , - 'dev-inspector': , - 'dev-fab': , -}; - -export function DevMenuView({ uuid, task }: Props) { - const context = useContext(DevMenuBottomSheetContext); - - const [enableDevMenuTools, setEnableDevMenuTools] = React.useState(false); - const [devMenuItems, setDevMenuItems] = React.useState<{ [key: string]: any }>({}); - const [isOnboardingFinished, setIsOnboardingFinished] = React.useState(false); - const [isLoaded, setIsLoaded] = React.useState(false); - - const theme = useExpoTheme(); - const insets = useSafeAreaInsets(); - - const prevUUIDRef = useRef(uuid); - - useEffect(function didMount() { - loadStateAsync(); - }, []); - - useEffect( - function loadStateWhenUUIDChanges() { - if (prevUUIDRef.current !== uuid) { - loadStateAsync(); - } - - prevUUIDRef.current = uuid; - }, - [uuid] - ); - - async function collapse() { - if (context) { - await context.collapse(); - } - } - - async function collapseAndCloseDevMenuAsync() { - await collapse(); - await DevMenu.closeAsync(); - } - - async function loadStateAsync() { - setIsLoaded(false); - - const [enableDevMenuTools, devMenuItems, isOnboardingFinished] = await Promise.all([ - DevMenu.doesCurrentTaskEnableDevtoolsAsync(), - DevMenu.getItemsToShowAsync(), - DevMenu.isOnboardingFinishedAsync(), - Font.loadAsync({ - 'Inter-Black': require('../assets/Inter/Inter-Black.otf'), - 'Inter-BlackItalic': require('../assets/Inter/Inter-BlackItalic.otf'), - 'Inter-Bold': require('../assets/Inter/Inter-Bold.otf'), - 'Inter-BoldItalic': require('../assets/Inter/Inter-BoldItalic.otf'), - 'Inter-ExtraBold': require('../assets/Inter/Inter-ExtraBold.otf'), - 'Inter-ExtraBoldItalic': require('../assets/Inter/Inter-ExtraBoldItalic.otf'), - 'Inter-ExtraLight': require('../assets/Inter/Inter-ExtraLight.otf'), - 'Inter-ExtraLightItalic': require('../assets/Inter/Inter-ExtraLightItalic.otf'), - 'Inter-Regular': require('../assets/Inter/Inter-Regular.otf'), - 'Inter-Italic': require('../assets/Inter/Inter-Italic.otf'), - 'Inter-Light': require('../assets/Inter/Inter-Light.otf'), - 'Inter-LightItalic': require('../assets/Inter/Inter-LightItalic.otf'), - 'Inter-Medium': require('../assets/Inter/Inter-Medium.otf'), - 'Inter-MediumItalic': require('../assets/Inter/Inter-MediumItalic.otf'), - 'Inter-SemiBold': require('../assets/Inter/Inter-SemiBold.otf'), - 'Inter-SemiBoldItalic': require('../assets/Inter/Inter-SemiBoldItalic.otf'), - 'Inter-Thin': require('../assets/Inter/Inter-Thin.otf'), - 'Inter-ThinItalic': require('../assets/Inter/Inter-ThinItalic.otf'), - }), - ]); - - setEnableDevMenuTools(enableDevMenuTools); - setDevMenuItems(devMenuItems); - setIsOnboardingFinished(isOnboardingFinished); - setIsLoaded(true); - } - - function onAppReload() { - collapse(); - DevMenu.reloadAppAsync(); - } - - function onGoToHome() { - collapse(); - DevMenu.goToHomeAsync(); - } - - function onPressDevMenuButton(key: string) { - DevMenu.selectItemWithKeyAsync(key); - } - - function onOnboardingFinished() { - DevMenu.setOnboardingFinishedAsync(true); - setIsOnboardingFinished(true); - } - - const sortedDevMenuItems = Object.keys(devMenuItems).sort( - (a, b) => DEV_MENU_ORDER.indexOf(a) - DEV_MENU_ORDER.indexOf(b) - ); - - if (!isLoaded) { - return null; - } - - return ( - - - - - - - - {!isOnboardingFinished ? ( - - ) : ( - - - - - - - - - - } - /> - - } - /> - - - {enableDevMenuTools && devMenuItems && ( - - - {sortedDevMenuItems.map((key, i) => { - const item = devMenuItems[key]; - - const { label, isEnabled } = item; - return ( - - - {i < sortedDevMenuItems.length - 1 && } - - ); - })} - - - )} - - - )} - - - ); -} diff --git a/apps/expo-go/src/navigation/BottomTabNavigator.android.ts b/apps/expo-go/src/navigation/BottomTabNavigator.android.ts deleted file mode 100644 index 407067ccd96363..00000000000000 --- a/apps/expo-go/src/navigation/BottomTabNavigator.android.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { darkTheme, lightTheme } from '@expo/styleguide-native'; -import { ComponentProps } from 'react'; -import { StyleSheet } from 'react-native'; -import { createMaterialBottomTabNavigator } from 'react-native-paper/react-navigation'; - -import Colors, { ColorTheme } from '../constants/Colors'; - -const BottomTabNavigator = createMaterialBottomTabNavigator(); - -export default BottomTabNavigator; - -export const getNavigatorProps = (props: { - theme: ColorTheme; -}): Partial> => ({ - shifting: true, - activeColor: props.theme === 'dark' ? darkTheme.link.default : lightTheme.link.default, - inactiveColor: props.theme === 'dark' ? darkTheme.icon.default : lightTheme.icon.default, - barStyle: { - borderTopWidth: - props.theme === 'dark' ? StyleSheet.hairlineWidth * 2 : StyleSheet.hairlineWidth, - borderTopColor: Colors[props.theme].cardSeparator, - backgroundColor: - props.theme === 'dark' ? darkTheme.background.default : lightTheme.background.default, - }, -}); diff --git a/apps/expo-go/src/navigation/BottomTabNavigator.ts b/apps/expo-go/src/navigation/BottomTabNavigator.ts deleted file mode 100644 index a8133d6950ed3c..00000000000000 --- a/apps/expo-go/src/navigation/BottomTabNavigator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { darkTheme, lightTheme } from '@expo/styleguide-native'; -import { ComponentProps } from 'react'; - -import { createNativeBottomTabsNavigator } from './NativeBottomTabsNavigator'; - -const BottomTabNavigator = createNativeBottomTabsNavigator(); -export default BottomTabNavigator; - -export const getNavigatorProps = (props: { - theme: string; -}): Partial> => ({ - tabBarStyle: { - backgroundColor: - props.theme === 'dark' ? darkTheme.background.default : lightTheme.background.default, - }, - tabBarActiveTintColor: props.theme === 'dark' ? darkTheme.link.default : lightTheme.link.default, - tabBarInactiveTintColor: - props.theme === 'dark' ? darkTheme.icon.default : lightTheme.icon.default, -}); diff --git a/apps/expo-go/src/navigation/NativeBottomTabsNavigator.tsx b/apps/expo-go/src/navigation/NativeBottomTabsNavigator.tsx deleted file mode 100644 index 2231e52cc2c35d..00000000000000 --- a/apps/expo-go/src/navigation/NativeBottomTabsNavigator.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { - createNavigatorFactory, - type NavigatorTypeBagBase, - type ParamListBase, - type StackNavigationState, - type StaticConfig, - TabRouter, - type TypedNavigator, - useNavigationBuilder, -} from '@react-navigation/native'; -import * as React from 'react'; -import { BottomTabs, BottomTabsScreen } from 'react-native-screens'; - -function NativeBottomTabsNavigator({ - id, - initialRouteName, - children, - layout, - screenListeners, - screenOptions, - screenLayout, - router, - ...rest -}: any) { - const { state, descriptors, navigation } = useNavigationBuilder(TabRouter, { - id, - initialRouteName, - children, - layout, - screenListeners, - screenOptions, - screenLayout, - }); - const { routes } = state; - const deferredFocusedIndex = React.useDeferredValue(state.index); - - return ( - { - const descriptor = descriptors[tabKey]; - const route = descriptor.route; - navigation.dispatch({ - type: 'JUMP_TO', - target: state.key, - payload: { - name: route.name, - }, - }); - }}> - {routes - .map((route, index) => ({ route, index })) - .map(({ route, index }) => { - const descriptor = descriptors[route.key]; - const isFocused = index === deferredFocusedIndex; - - return ( - - {descriptor.render()} - - ); - })} - - ); -} - -export function createNativeBottomTabsNavigator< - const ParamList extends ParamListBase, - const NavigatorID extends string | undefined = undefined, - const TypeBag extends NavigatorTypeBagBase = { - ParamList: ParamList; - NavigatorID: NavigatorID; - State: StackNavigationState; - ScreenOptions: any; - EventMap: any; - NavigationList: { - [RouteName in keyof ParamList]: any; - }; - Navigator: any; - }, - const Config extends StaticConfig = StaticConfig, ->(config?: Config): TypedNavigator { - return createNavigatorFactory(NativeBottomTabsNavigator)(config); -} diff --git a/apps/expo-go/src/navigation/Navigation.tsx b/apps/expo-go/src/navigation/Navigation.tsx deleted file mode 100644 index b4da6f85cc0c68..00000000000000 --- a/apps/expo-go/src/navigation/Navigation.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import { HomeFilledIcon, SettingsFilledIcon } from '@expo/styleguide-native'; -import { NavigationContainer, useTheme, useNavigationContainerRef } from '@react-navigation/native'; -import { createStackNavigator, TransitionPresets } from '@react-navigation/stack'; -import * as React from 'react'; -import { Platform, StyleSheet, Linking } from 'react-native'; - -import BottomTab, { getNavigatorProps } from './BottomTabNavigator'; -import { HomeStackRoutes, SettingsStackRoutes, ModalStackRoutes } from './Navigation.types'; -import defaultNavigationOptions from './defaultNavigationOptions'; -import DiagnosticsIcon from '../components/Icons'; -import { ColorTheme } from '../constants/Colors'; -import Themes from '../constants/Themes'; -import { AccountModal } from '../screens/AccountModal'; -import { BranchDetailsScreen } from '../screens/BranchDetailsScreen'; -import { BranchListScreen } from '../screens/BranchListScreen'; -import { DiagnosticsStackScreen } from '../screens/DiagnosticsScreen'; -import { FeedbackFormScreen } from '../screens/FeedbackFormScreen'; -import { HomeScreen } from '../screens/HomeScreen'; -import { ProjectScreen } from '../screens/ProjectScreen'; -import { ProjectsListScreen } from '../screens/ProjectsListScreen'; -import QRCodeScreen from '../screens/QRCodeScreen'; -import { SettingsScreen } from '../screens/SettingsScreen'; -import { SnacksListScreen } from '../screens/SnacksListScreen'; -import { - alertWithCameraPermissionInstructions, - requestCameraPermissionsAsync, -} from '../utils/PermissionUtils'; - -// TODO(Bacon): Do we need to create a new one each time? -const HomeStack = createStackNavigator(); -const SettingsStack = createStackNavigator(); - -// We have to disable this option on Android to not use `react-native-screen`, -// which aren't correcly installed in the Home app. -const shouldDetachInactiveScreens = Platform.OS !== 'android'; - -function useThemeName() { - const theme = useTheme(); - return theme.dark ? ColorTheme.DARK : ColorTheme.LIGHT; -} - -function HomeStackScreen() { - const themeName = useThemeName(); - - return ( - - - - - - - - - - ); -} - -function SettingsStackScreen() { - const themeName = useThemeName(); - - return ( - - <>, - }} - /> - - ); -} - -const RootStack = createStackNavigator(); - -function TabNavigator(props: { theme: string }) { - return ( - - - Platform.OS === 'ios' ? ( - { - ios: { - type: 'sfSymbol', - name: 'house.fill', - }, - } - ) : ( - - ), - tabBarLabel: 'Home', - }} - /> - - {Platform.OS === 'ios' && ( - - Platform.OS === 'ios' ? ( - { ios: { name: 'ecg.text.page.fill', type: 'sfSymbol' } } - ) : ( - - ), - tabBarLabel: 'Diagnostics', - }} - /> - )} - - Platform.OS === 'ios' ? ( - { ios: { name: 'gearshape.fill', type: 'sfSymbol' } } - ) : ( - - ), - tabBarLabel: 'Settings', - }} - /> - - ); -} - -const ModalStack = createStackNavigator(); - -export default (props: { theme: ColorTheme }) => { - const navigationRef = useNavigationContainerRef(); - const isNavigationReadyRef = React.useRef(false); - const initialURLWasConsumed = React.useRef(false); - - React.useEffect(() => { - const handleDeepLinks = async ({ url }: { url: string | null }) => { - if (Platform.OS === 'ios' || !url || !isNavigationReadyRef.current) { - return; - } - const nav = navigationRef.current; - if (!nav) { - return; - } - - if (url.startsWith('expo-home://qr-scanner')) { - if (await requestCameraPermissionsAsync()) { - nav.navigate('QRCode'); - } else { - await alertWithCameraPermissionInstructions(); - } - } - }; - if (!initialURLWasConsumed.current) { - initialURLWasConsumed.current = true; - Linking.getInitialURL().then((url) => { - handleDeepLinks({ url }); - }); - } - - const deepLinkSubscription = Linking.addEventListener('url', handleDeepLinks); - - return () => { - isNavigationReadyRef.current = false; - deepLinkSubscription.remove(); - }; - }, []); - - return ( - { - isNavigationReadyRef.current = true; - }}> - ({ - headerShown: false, - gestureEnabled: true, - cardOverlayEnabled: true, - cardStyle: { backgroundColor: 'transparent' }, - presentation: 'modal', - // NOTE(brentvatne): it is unclear what this was intended for, it doesn't appear to be needed? - // headerStatusBarHeight: navigation.getState().routes.indexOf(route) > 0 ? 0 : undefined, - ...TransitionPresets.ModalPresentationIOS, - })}> - - {() => ( - - - {() => } - - ({ - headerShown: false, - ...(Platform.OS === 'ios' && { - gestureEnabled: true, - cardOverlayEnabled: true, - // NOTE(brentvatne): it is unclear what this was intended for, it doesn't appear to be needed? - // headerStatusBarHeight: - // navigation - // .getState() - // .routes.findIndex((r: RouteProp) => r.key === route.key) > 0 - // ? 0 - // : undefined, - ...TransitionPresets.ModalPresentationIOS, - }), - })} - /> - - )} - - - - - ); -}; - -const styles = StyleSheet.create({ - icon: { - marginBottom: Platform.OS === 'ios' ? -3 : 0, - }, -}); diff --git a/apps/expo-go/src/navigation/Navigation.types.ts b/apps/expo-go/src/navigation/Navigation.types.ts deleted file mode 100644 index 017ea4a7ca6c71..00000000000000 --- a/apps/expo-go/src/navigation/Navigation.types.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type ModalStackRoutes = { - QRCode: undefined; - RootStack: undefined; -}; - -export type HomeStackRoutes = { - Home: undefined; - ProjectsList: { accountName: string }; - SnacksList: { accountName: string }; - ProjectDetails: { id: string }; - Branches: { appId: string }; - BranchDetails: { appId: string; branchName: string }; - Account: undefined; - Project: { id: string }; - FeedbackForm: undefined; -}; - -export type SettingsStackRoutes = { - Settings: undefined; - DeleteAccount: { viewerUsername: string }; -}; - -export type DiagnosticsStackRoutes = { - Diagnostics: object; - Audio: object; - Location: object; - Geofencing: object; -}; diff --git a/apps/expo-go/src/navigation/defaultNavigationOptions.tsx b/apps/expo-go/src/navigation/defaultNavigationOptions.tsx deleted file mode 100644 index d626d2fcc720c5..00000000000000 --- a/apps/expo-go/src/navigation/defaultNavigationOptions.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { darkTheme, lightTheme } from '@expo/styleguide-native'; -import { - StackNavigationOptions, - HeaderStyleInterpolators, - Header, - StackHeaderProps, -} from '@react-navigation/stack'; -import { Platform, StyleSheet, ViewStyle } from 'react-native'; - -import { CappedWidthContainerView } from '../components/Views'; -import { ColorTheme } from '../constants/Colors'; - -export default (theme: ColorTheme): StackNavigationOptions => { - const androidHeader = (props: StackHeaderProps) => ( - -
- - ); - - return { - // On iOS the header title is centered by default so we can skip adding padding to it - header: Platform.OS === 'android' ? androidHeader : undefined, - headerStyle: { - elevation: 0, - // On android the border is added in the `androidHeader` component above - borderBottomWidth: Platform.OS === 'android' ? 0 : StyleSheet.hairlineWidth, - borderBottomColor: theme === 'dark' ? darkTheme.border.default : lightTheme.border.default, - backgroundColor: - theme === 'dark' ? darkTheme.background.default : lightTheme.background.default, - }, - headerTitleStyle: { - fontWeight: Platform.OS === 'ios' ? '600' : '400', - fontFamily: 'Inter-SemiBold', - color: theme === 'dark' ? darkTheme.text.default : lightTheme.text.default, - }, - headerTintColor: theme === 'dark' ? darkTheme.icon.default : lightTheme.icon.default, - headerBackButtonDisplayMode: 'minimal', - headerStyleInterpolator: HeaderStyleInterpolators.forUIKit, - }; -}; diff --git a/apps/expo-go/src/redux/HistoryActions.ts b/apps/expo-go/src/redux/HistoryActions.ts deleted file mode 100644 index f2fda1509c597a..00000000000000 --- a/apps/expo-go/src/redux/HistoryActions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AppDispatch, AppThunk } from './Store.types'; -import LocalStorage from '../storage/LocalStorage'; -import { HistoryItem } from '../types'; -import { Manifest } from '../types/Manifest'; - -export default { - loadHistory(): AppThunk { - return async (dispatch: AppDispatch) => { - const history = await LocalStorage.getHistoryAsync(); - return dispatch({ - type: 'loadHistory', - payload: { history }, - }); - }; - }, - - clearHistory(): AppThunk { - return async (dispatch: AppDispatch) => { - await LocalStorage.clearHistoryAsync(); - return dispatch({ - type: 'clearHistory', - }); - }; - }, - - addHistoryItem(manifestUrl: string, manifest: Manifest): AppThunk { - return async (dispatch: AppDispatch) => { - const historyItem: HistoryItem = { - manifestUrl, - manifest, - url: manifestUrl, - time: Date.now(), - }; - - let history = await LocalStorage.getHistoryAsync(); - history = history.filter((item) => item.url !== historyItem.url); - history.unshift(historyItem); - await LocalStorage.saveHistoryAsync(history); - - return dispatch({ - type: 'loadHistory', - payload: { history }, - }); - }; - }, -}; diff --git a/apps/expo-go/src/redux/HistoryReducer.ts b/apps/expo-go/src/redux/HistoryReducer.ts deleted file mode 100644 index 09b648a7c50644..00000000000000 --- a/apps/expo-go/src/redux/HistoryReducer.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { List, Record } from 'immutable'; - -import { HistoryItem as HistoryItemInput } from '../types'; - -type HistoryItemObject = { - url: null | string; - bundleUrl: null | string; - manifestUrl: null | string; - manifest: null | { [key: string]: any }; - time: null | number; -}; - -type HistoryItemType = Record & Readonly; - -const HistoryItem = Record({ - url: null, - bundleUrl: null, - manifestUrl: null, - manifest: null, - time: null, -}); - -type HistoryObject = { - history: List; -}; - -export type HistoryType = Record & Readonly; - -const HistoryState = Record({ - history: List(), -}); - -type HistoryActions = - | { - type: 'loadHistory'; - payload: { history: HistoryItemInput[] }; - } - | { type: 'clearHistory' }; - -export default (state: HistoryType, action: HistoryActions): HistoryType => { - switch (action.type) { - case 'loadHistory': { - const { history } = action.payload; - const immutableHistoryList = history - ? List(history.map((item) => new HistoryItem(item))) - : List(); - return state.merge({ - // @ts-ignore - history: immutableHistoryList, - }); - } - case 'clearHistory': - return state.merge({ - history: state.get('history').clear(), - }); - default: - return state ? state : new HistoryState(); - } -}; diff --git a/apps/expo-go/src/redux/Hooks.ts b/apps/expo-go/src/redux/Hooks.ts deleted file mode 100644 index 9fa5dfa5081837..00000000000000 --- a/apps/expo-go/src/redux/Hooks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - TypedUseSelectorHook, - useDispatch as useUntypedDispatch, - useSelector as useUntypedSelector, -} from 'react-redux'; -import { AnyAction } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; - -import { RootState } from './Store.types'; - -export const useSelector: TypedUseSelectorHook = useUntypedSelector; - -export const useDispatch = (): ThunkDispatch => useUntypedDispatch(); diff --git a/apps/expo-go/src/redux/SessionActions.ts b/apps/expo-go/src/redux/SessionActions.ts deleted file mode 100644 index bb79e24ce9f983..00000000000000 --- a/apps/expo-go/src/redux/SessionActions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { SessionObject } from './SessionReducer'; -import { AppDispatch, AppThunk } from './Store.types'; -import ApolloClient from '../api/ApolloClient'; -import AuthApi from '../api/AuthApi'; -import LocalStorage from '../storage/LocalStorage'; - -export default { - setSession(session: SessionObject): AppThunk { - return async (dispatch: AppDispatch) => { - await LocalStorage.saveSessionAsync(session); - return dispatch({ - type: 'setSession', - payload: session, - }); - }; - }, - - signOut(): AppThunk { - return async (dispatch: AppDispatch) => { - const session = await LocalStorage.getSessionAsync(); - if (session) { - try { - await AuthApi.signOutAsync(session.sessionSecret); - } catch (e) { - // continue to clear out session in redux and local storage even if API logout fails - console.error('Something went wrong when signing out:', e); - } - await LocalStorage.removeSessionAsync(); - } - - await LocalStorage.clearHistoryAsync(); - - ApolloClient.resetStore(); - - return dispatch({ - type: 'signOut', - payload: null, - }); - }; - }, -}; diff --git a/apps/expo-go/src/redux/SessionReducer.ts b/apps/expo-go/src/redux/SessionReducer.ts deleted file mode 100644 index 79f128f773401b..00000000000000 --- a/apps/expo-go/src/redux/SessionReducer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Record } from 'immutable'; - -export type SessionObject = { - sessionSecret: string | null; -}; - -export type SessionType = Record & Readonly; - -const SessionState = Record({ - sessionSecret: null, -}); - -type SessionActions = - | { - type: 'setSession'; - payload: SessionObject; - } - | { type: 'signOut' }; - -export default (state: SessionType = new SessionState(), action: SessionActions): SessionType => { - switch (action.type) { - case 'setSession': - return new SessionState(action.payload); - case 'signOut': - return new SessionState(); - default: - return state; - } -}; diff --git a/apps/expo-go/src/redux/SettingsActions.ts b/apps/expo-go/src/redux/SettingsActions.ts deleted file mode 100644 index c911b2b46620bc..00000000000000 --- a/apps/expo-go/src/redux/SettingsActions.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Appearance } from 'react-native'; - -import { AppDispatch, AppThunk } from './Store.types'; -import * as DevMenu from '../menu/DevMenuModule'; -import LocalStorage from '../storage/LocalStorage'; - -type ColorSchemeName = Appearance.AppearancePreferences['colorScheme']; - -export default { - loadSettings(): AppThunk { - return async (dispatch: AppDispatch) => { - const [localStorageSettings, devMenuSettings] = await Promise.all([ - LocalStorage.getSettingsAsync(), - DevMenu.getSettingsAsync(), - ]); - - return dispatch({ - type: 'loadSettings', - payload: { - ...localStorageSettings, - preferredAppearance: localStorageSettings.preferredAppearance ?? undefined, - devMenuSettings, - }, - }); - }; - }, - - setPreferredAppearance(preferredAppearance: ColorSchemeName): AppThunk { - return async (dispatch: AppDispatch) => { - try { - await LocalStorage.updateSettingsAsync({ - preferredAppearance, - }); - - dispatch({ - type: 'setPreferredAppearance', - payload: { preferredAppearance }, - }); - } catch { - alert('Oops, something went wrong and we were unable to change the preferred appearance'); - } - }; - }, - - setDevMenuSetting(key: keyof DevMenu.DevMenuSettings, value?: boolean): AppThunk { - return async (dispatch: AppDispatch) => { - try { - await DevMenu.setSettingAsync(key, value); - - dispatch({ - type: 'setDevMenuSettings', - payload: { [key]: value }, - }); - } catch { - alert('Oops, something went wrong and we were unable to change dev menu settings!'); - } - }; - }, -}; diff --git a/apps/expo-go/src/redux/SettingsReducer.ts b/apps/expo-go/src/redux/SettingsReducer.ts deleted file mode 100644 index ce478dc38b3438..00000000000000 --- a/apps/expo-go/src/redux/SettingsReducer.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Record } from 'immutable'; -import { Appearance } from 'react-native'; - -type ColorSchemeName = Appearance.AppearancePreferences['colorScheme']; - -type SettingsObject = { - preferredAppearance: null | ColorSchemeName; - devMenuSettings: null | { - motionGestureEnabled?: boolean; - touchGestureEnabled?: boolean; - }; -}; - -export type SettingsType = Record & Readonly; - -type SettingsActions = - | { - type: 'loadSettings'; - payload: SettingsObject; - } - | { type: 'setPreferredAppearance'; payload: Pick } - | { type: 'setDevMenuSettings'; payload: SettingsObject['devMenuSettings'] }; - -const SettingsState = Record({ - preferredAppearance: null, - devMenuSettings: null, -}); - -export default (state: SettingsType, action: SettingsActions): SettingsType => { - switch (action.type) { - case 'loadSettings': - return new SettingsState(action.payload); - case 'setPreferredAppearance': { - const { preferredAppearance } = action.payload; - return state.merge({ preferredAppearance }); - } - case 'setDevMenuSettings': { - const devMenuSettings = state.get('devMenuSettings'); - return state.set('devMenuSettings', { - ...devMenuSettings, - ...action.payload, - }); - } - default: - return state || new SettingsState(); - } -}; diff --git a/apps/expo-go/src/redux/Store.ts b/apps/expo-go/src/redux/Store.ts deleted file mode 100644 index db2f8ecb65d032..00000000000000 --- a/apps/expo-go/src/redux/Store.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2015-present 650 Industries. All rights reserved. - */ - -import { applyMiddleware, combineReducers, createStore } from 'redux'; -import thunk from 'redux-thunk'; - -import HistoryReducer from './HistoryReducer'; -import SessionReducer from './SessionReducer'; -import SettingsReducer from './SettingsReducer'; - -const reduce = combineReducers({ - history: HistoryReducer, - session: SessionReducer, - settings: SettingsReducer, -}); - -export default createStore(reduce, applyMiddleware(thunk)); diff --git a/apps/expo-go/src/redux/Store.types.ts b/apps/expo-go/src/redux/Store.types.ts deleted file mode 100644 index 3153d328467e5c..00000000000000 --- a/apps/expo-go/src/redux/Store.types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Action } from 'redux'; -import { ThunkAction } from 'redux-thunk'; - -import { HistoryType } from './HistoryReducer'; -import { SessionType } from './SessionReducer'; -import { SettingsType } from './SettingsReducer'; -import Store from './Store'; - -export interface RootState { - history: HistoryType; - session: SessionType; - settings: SettingsType; -} - -export type AppThunk = ThunkAction< - ReturnType, - RootState, - unknown, - Action ->; - -export type AppDispatch = typeof Store.dispatch; diff --git a/apps/expo-go/src/redux/__mocks__/Store.ts b/apps/expo-go/src/redux/__mocks__/Store.ts deleted file mode 100644 index a82b9faad8b026..00000000000000 --- a/apps/expo-go/src/redux/__mocks__/Store.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default { - getState: jest.fn(() => ({ - history: { history: [] }, - session: { sessionSecret: null }, - settings: { preferredAppearance: undefined }, - })), - dispatch: jest.fn((action) => action), - subscribe: jest.fn(() => jest.fn()), - replaceReducer: jest.fn(), -}; diff --git a/apps/expo-go/src/screens/AccountModal/LoggedInAccountView.tsx b/apps/expo-go/src/screens/AccountModal/LoggedInAccountView.tsx deleted file mode 100644 index c153c01e8356d4..00000000000000 --- a/apps/expo-go/src/screens/AccountModal/LoggedInAccountView.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { borderRadius, CheckIcon, iconSize, spacing, UsersIcon } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { Text, View, Image, useExpoTheme, Row, Spacer, Divider } from 'expo-dev-client-components'; -import React from 'react'; -import { FlatList } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { SectionHeader } from '../../components/SectionHeader'; -import { Home_CurrentUserActorQuery } from '../../graphql/types'; -import { useDispatch } from '../../redux/Hooks'; -import SessionActions from '../../redux/SessionActions'; -import { useAccountName } from '../../utils/AccountNameContext'; - -type Props = { - accounts: Exclude['accounts']; -}; - -export function LoggedInAccountView({ accounts }: Props) { - const { accountName, setAccountName } = useAccountName(); - const navigation = useNavigation(); - const theme = useExpoTheme(); - const dispatch = useDispatch(); - - const onSignOutPress = React.useCallback(() => { - setAccountName(undefined); - dispatch(SessionActions.signOut()); - }, [dispatch]); - - return ( - - - - data={accounts} - contentContainerStyle={{ padding: spacing[4] }} - ListHeaderComponent={() => ( - <> - - - - Log Out - - - - - - )} - keyExtractor={(account) => account.id} - renderItem={({ item: account, index }) => ( - { - setAccountName(account.name); - navigation.goBack(); - }}> - - - {account?.ownerUserActor?.profilePhoto ? ( - - ) : ( - - - - )} - - - {account.ownerUserActor ? ( - <> - {account.ownerUserActor.fullName ? ( - <> - - {account.ownerUserActor.fullName} - - - - {account.ownerUserActor.username} - - - ) : ( - - {account.ownerUserActor.username} - - )} - - ) : ( - - {account.name} - - )} - - - {accountName === account.name && ( - - )} - - - )} - ItemSeparatorComponent={() => } - /> - - - ); -} diff --git a/apps/expo-go/src/screens/AccountModal/LoggedOutAccountView.tsx b/apps/expo-go/src/screens/AccountModal/LoggedOutAccountView.tsx deleted file mode 100644 index d14a367f2bb1e0..00000000000000 --- a/apps/expo-go/src/screens/AccountModal/LoggedOutAccountView.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { borderRadius, spacing } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { View, Text, Spacer, useExpoTheme } from 'expo-dev-client-components'; -import * as WebBrowser from 'expo-web-browser'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import url from 'url'; - -import ApolloClient from '../../api/ApolloClient'; -import Config from '../../api/Config'; -import { - Home_ViewerPrimaryAccountNameDocument, - Home_ViewerPrimaryAccountNameQuery, - Home_ViewerPrimaryAccountNameQueryVariables, -} from '../../graphql/types'; -import { useDispatch, useSelector } from '../../redux/Hooks'; -import SessionActions from '../../redux/SessionActions'; -import { useAccountName } from '../../utils/AccountNameContext'; -import hasSessionSecret from '../../utils/hasSessionSecret'; -import { isQuest } from '../../utils/isQuest'; -import { openQuestAuthSessionAsync } from '../../utils/questAuthSessionPolyfill'; - -type Props = { - refetch: () => Promise; -}; - -export function LoggedOutAccountView({ refetch }: Props) { - const dispatch = useDispatch(); - const [isAuthenticating, setIsAuthenticating] = React.useState(false); - const [isFinishedAuthenticating, setIsFinishedAuthenticating] = React.useState(false); - const [authenticationError, setAuthenticationError] = React.useState(null); - const mounted = React.useRef(true); - const theme = useExpoTheme(); - const { setAccountName } = useAccountName(); - const navigation = useNavigation(); - - const { sessionSecretExists } = useSelector((data) => { - const sessionSecretExists = hasSessionSecret(data.session); - return { - sessionSecretExists, - }; - }); - - React.useEffect(() => { - async function refetchThenGoBackAsync() { - // after logging in, wait for redux action to dispatch, refetch with new sessionSecret, then dismiss modal - if (isFinishedAuthenticating && sessionSecretExists) { - try { - await refetch(); - } finally { - // in the case that it rejects, we still want to dismiss the modal - - // if it's an internet issue, the user will be able to try to refresh the homepage - - // if it's an issue with the sessionSecret being invalid, the user will be able to try to - // log in again and rewrite the sessionSecret - - navigation.goBack(); - } - } - } - - refetchThenGoBackAsync(); - }, [isFinishedAuthenticating, sessionSecretExists]); - - React.useEffect(() => { - mounted.current = true; - return () => { - mounted.current = false; - }; - }, []); - - const _handleSignInPress = async () => { - await _handleAuthentication('login'); - }; - - const _handleSignUpPress = async () => { - await _handleAuthentication('signup'); - }; - - const _handleAuthentication = async (urlPath: string) => { - if (isAuthenticating) { - return; - } - setAuthenticationError(null); - setIsAuthenticating(true); - - try { - const redirectBase = 'expauth://auth'; - const authSessionURL = `${ - Config.website.origin - }/${urlPath}?confirm_account=1&app_redirect_uri=${encodeURIComponent(redirectBase)}`; - - // Use a custom auth session opener for Meta Quest devices (see note in `openQuestAuthSessionAsync`) - const openAuthSessionAsync = isQuest() - ? openQuestAuthSessionAsync - : WebBrowser.openAuthSessionAsync; - const result = await openAuthSessionAsync(authSessionURL, redirectBase, { - /** note(brentvatne): We should disable the showInRecents option when - * https://github.com/expo/expo/issues/8072 is resolved. This workaround - * prevents the Chrome Custom Tabs activity from closing when the user - * switches from the login / sign up form to a password manager or 2fa - * app. The downside of using this flag is that the browser window will - * remain open in the background after authentication completes. */ - showInRecents: true, - }); - - if (!mounted.current) { - return; - } - - if (result.type === 'success') { - const resultURL = url.parse(result.url, true); - const encodedSessionSecret = resultURL.query['session_secret'] as string; - if (!encodedSessionSecret) { - throw new Error('session_secret is missing in auth redirect query'); - } - - const sessionSecret = decodeURIComponent(encodedSessionSecret); - - const viewerPrimaryAccountNameResult = await ApolloClient.query< - Home_ViewerPrimaryAccountNameQuery, - Home_ViewerPrimaryAccountNameQueryVariables - >({ - query: Home_ViewerPrimaryAccountNameDocument, - fetchPolicy: 'network-only', - context: { - headers: { 'expo-session': sessionSecret }, - }, - }); - - if ( - viewerPrimaryAccountNameResult.errors && - viewerPrimaryAccountNameResult.errors.length > 0 - ) { - throw viewerPrimaryAccountNameResult.errors[0]; - } - - const primaryAccountName = - viewerPrimaryAccountNameResult.data.meUserActor?.primaryAccount.name; - if (!primaryAccountName) { - throw new Error('Logged in user must have a primary account'); - } - - dispatch( - SessionActions.setSession({ - sessionSecret, - }) - ); - - setAccountName(primaryAccountName); - setIsFinishedAuthenticating(true); - } - } catch (e: any) { - // TODO(wschurman): Put this into Sentry - console.error({ e }); - setAuthenticationError(e.message); - } finally { - setIsAuthenticating(false); - } - }; - - return ( - - - Log in or create an account to access your projects, view local development servers, and - more. - - - - - - Log In - - - - - - - - Sign Up - - - - {authenticationError && ( - <> - - - Something went wrong when authenticating: {authenticationError} - - - )} - - ); -} diff --git a/apps/expo-go/src/screens/AccountModal/ModalHeader.tsx b/apps/expo-go/src/screens/AccountModal/ModalHeader.tsx deleted file mode 100644 index 9fc9a446a81100..00000000000000 --- a/apps/expo-go/src/screens/AccountModal/ModalHeader.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { iconSize, spacing, XIcon } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { Text, Row, useExpoTheme } from 'expo-dev-client-components'; -import React from 'react'; -import { Platform, StyleSheet } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { CappedWidthContainerView } from '../../components/Views'; - -export function ModalHeader() { - const theme = useExpoTheme(); - const navigation = useNavigation(); - const insets = useSafeAreaInsets(); - return ( - - - - Account - - navigation.goBack()}> - - - - - ); -} diff --git a/apps/expo-go/src/screens/AccountModal/index.tsx b/apps/expo-go/src/screens/AccountModal/index.tsx deleted file mode 100644 index 90b8ac50dcafcd..00000000000000 --- a/apps/expo-go/src/screens/AccountModal/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import Ionicons from '@expo/vector-icons/build/Ionicons'; -import { Text, View, useExpoTheme, Row, Spacer } from 'expo-dev-client-components'; -import React from 'react'; -import { ActivityIndicator, Platform } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { LoggedInAccountView } from './LoggedInAccountView'; -import { LoggedOutAccountView } from './LoggedOutAccountView'; -import { ModalHeader } from './ModalHeader'; -import { CappedWidthContainerView } from '../../components/Views'; -import { useHome_CurrentUserActorQuery } from '../../graphql/types'; - -export function AccountModal() { - const theme = useExpoTheme(); - - const { data, loading, error, refetch } = useHome_CurrentUserActorQuery({ - fetchPolicy: 'cache-and-network', - }); - - if (loading) { - return ( - - - - - - - ); - } - - if (error) { - console.error(error); - - return ( - - {Platform.OS === 'ios' && } - - - - - - We couldn't load your accounts. - - - - - {error.message} - - - refetch()}> - - - Try again - - - - - - - ); - } - - // if data.viewer is undefined, then the user is not authenticated, so show the login screen - - return ( - - - - {data?.meUserActor?.accounts ? ( - - ) : ( - { - await refetch(); - }} - /> - )} - - - ); -} diff --git a/apps/expo-go/src/screens/BranchDetailsScreen/BranchDetailsContainer.tsx b/apps/expo-go/src/screens/BranchDetailsScreen/BranchDetailsContainer.tsx deleted file mode 100644 index 8a2e789c60a569..00000000000000 --- a/apps/expo-go/src/screens/BranchDetailsScreen/BranchDetailsContainer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import * as React from 'react'; -import { Platform } from 'react-native'; - -import { BranchDetailsView } from './BranchDetailsView'; -import { AppPlatform, useBranchDetailsQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -export function BranchDetailsContainer( - props: { appId: string; branchName: string } & StackScreenProps -) { - const query = useBranchDetailsQuery({ - fetchPolicy: 'cache-and-network', - notifyOnNetworkStatusChange: true, - variables: { - appId: props.appId, - name: props.branchName, - platform: Platform.OS === 'ios' ? AppPlatform.Ios : AppPlatform.Android, - }, - }); - - return ; -} diff --git a/apps/expo-go/src/screens/BranchDetailsScreen/BranchDetailsView.tsx b/apps/expo-go/src/screens/BranchDetailsScreen/BranchDetailsView.tsx deleted file mode 100644 index 007b3ac7b587d8..00000000000000 --- a/apps/expo-go/src/screens/BranchDetailsScreen/BranchDetailsView.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { NetworkStatus } from '@apollo/client'; -import { spacing } from '@expo/styleguide-native'; -import { StackScreenProps } from '@react-navigation/stack'; -import dedent from 'dedent'; -import { Divider, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import * as React from 'react'; -import { ActivityIndicator, RefreshControl, FlatList } from 'react-native'; - -import { BranchHeader } from './BranchHeader'; -import { EmptySection } from './EmptySection'; -import { SectionHeader } from '../../components/SectionHeader'; -import { UpdateListItem } from '../../components/UpdateListItem'; -import { CappedWidthContainerView } from '../../components/Views'; -import { BranchDetailsQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; -import { useThrottle } from '../../utils/useThrottle'; - -const ERROR_TEXT = dedent` - An unexpected error has occurred. - Sorry about this. We will resolve the issue as soon as possible. -`; - -type Props = { - loading: boolean; - error?: Error; - data?: BranchDetailsQuery; - branchName: string; - networkStatus: number; - refetch: () => Promise; -} & StackScreenProps; - -export function BranchDetailsView({ error, data, refetch, branchName, networkStatus }: Props) { - const theme = useExpoTheme(); - - const refetching = useThrottle(networkStatus === NetworkStatus.refetch, 800); - - if (error && !data?.app?.byId.updateBranchByName) { - console.error(error); - return ( - - - {ERROR_TEXT} - - - ); - } - - if (networkStatus === NetworkStatus.loading || !data?.app?.byId.updateBranchByName) { - return ( - - - - ); - } - - return ( - - - - } - ListHeaderComponent={} - keyExtractor={(update) => update.id} - contentContainerStyle={{ padding: spacing[4] }} - ItemSeparatorComponent={() => } - ListEmptyComponent={() => } - renderItem={({ item: update, index }) => ( - - )} - /> - - - ); -} diff --git a/apps/expo-go/src/screens/BranchDetailsScreen/BranchHeader.tsx b/apps/expo-go/src/screens/BranchDetailsScreen/BranchHeader.tsx deleted file mode 100644 index f1ef415edc41c9..00000000000000 --- a/apps/expo-go/src/screens/BranchDetailsScreen/BranchHeader.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { BranchIcon, iconSize, spacing } from '@expo/styleguide-native'; -import { Row, useExpoTheme, Text, Spacer, padding } from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import { BranchDetailsQuery } from 'src/graphql/types'; -import { - isUpdateCompatibleWithThisExpoGo, - openUpdateManifestPermalink, -} from 'src/utils/UpdateUtils'; - -import { CappedWidthContainerView } from '../../components/Views'; - -type Props = { - name: string; - latestUpdate?: NonNullable['updates'][0]; -}; - -export function BranchHeader(props: Props) { - const theme = useExpoTheme(); - - const latestUpdate = props.latestUpdate; - const openButton = - latestUpdate && isUpdateCompatibleWithThisExpoGo(latestUpdate) ? ( - { - openUpdateManifestPermalink(latestUpdate); - }} - style={{ - backgroundColor: theme.button.tertiary.background, - paddingHorizontal: spacing[4], - paddingVertical: spacing[2], - borderRadius: 4, - }}> - - Open - - - ) : null; - - return ( - - - - - - - {props.name} - - - {openButton} - - - ); -} diff --git a/apps/expo-go/src/screens/BranchDetailsScreen/EmptySection.tsx b/apps/expo-go/src/screens/BranchDetailsScreen/EmptySection.tsx deleted file mode 100644 index ca7365e1f447cf..00000000000000 --- a/apps/expo-go/src/screens/BranchDetailsScreen/EmptySection.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import dedent from 'dedent'; -import { useExpoTheme, Text, Spacer, View } from 'expo-dev-client-components'; -import * as WebBrowser from 'expo-web-browser'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -const NO_UPDATES_TEXT = dedent` -This branch has no updates. -`; - -export function EmptySection() { - const theme = useExpoTheme(); - - return ( - - {NO_UPDATES_TEXT} - - { - WebBrowser.openBrowserAsync( - 'https://docs.expo.dev/eas-update/getting-started/#publish-an-update' - ); - }} - style={{ - padding: spacing[2], - alignSelf: 'flex-start', - backgroundColor: theme.button.ghost.background, - borderWidth: 1, - borderColor: theme.button.ghost.border, - borderRadius: 4, - }}> - - Learn more - - - - ); -} diff --git a/apps/expo-go/src/screens/BranchDetailsScreen/index.tsx b/apps/expo-go/src/screens/BranchDetailsScreen/index.tsx deleted file mode 100644 index c88db6bcc7ac4a..00000000000000 --- a/apps/expo-go/src/screens/BranchDetailsScreen/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import * as React from 'react'; - -import { BranchDetailsContainer } from './BranchDetailsContainer'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -export function BranchDetailsScreen(props: StackScreenProps) { - const { appId, branchName } = props.route.params; - - return ; -} diff --git a/apps/expo-go/src/screens/BranchListScreen/BranchList.tsx b/apps/expo-go/src/screens/BranchListScreen/BranchList.tsx deleted file mode 100644 index 2aeb8186bd2048..00000000000000 --- a/apps/expo-go/src/screens/BranchListScreen/BranchList.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { ApolloQueryResult } from '@apollo/client'; -import { useNavigation } from '@react-navigation/native'; -import { StackNavigationProp } from '@react-navigation/stack'; -import * as React from 'react'; -import { Platform } from 'react-native'; - -import { BranchListView, BranchManifest } from './BranchListView'; -import { - AppPlatform, - BranchesForProjectQuery, - useBranchesForProjectQuery, -} from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -function useBranchesQuery({ appId, platform }: { appId: string; platform: AppPlatform }): { - branchManifests: BranchManifest[]; - loadMoreAsync: () => Promise>; -} { - const { data, fetchMore } = useBranchesForProjectQuery({ - variables: { - appId, - platform, - limit: 15, - offset: 0, - }, - fetchPolicy: 'cache-and-network', - }); - const navigation = useNavigation>(); - - const app = data?.app.byId; - const branchManifests: BranchManifest[] = - app?.updateBranches.map((branch) => ({ - name: branch.name, - id: branch.id, - latestUpdate: branch.updates[0], - })) ?? []; - - const loadMoreAsync = React.useCallback(() => { - return fetchMore({ - variables: { - offset: app?.updateBranches.length ?? 0, - }, - }); - }, [fetchMore, app]); - - React.useEffect(() => { - if (data?.app.byId) { - const fullName = data?.app.byId.fullName; - const title = `Branches - ${data?.app.byId.name ?? fullName}`; - navigation.setOptions({ - title, - }); - } - }, [navigation, data?.app.byId]); - - return { - branchManifests, - loadMoreAsync, - }; -} - -export function BranchList({ appId }: { appId: string }) { - const { branchManifests, loadMoreAsync } = useBranchesQuery({ - appId, - platform: Platform.OS === 'ios' ? AppPlatform.Ios : AppPlatform.Android, - }); - - return ; -} diff --git a/apps/expo-go/src/screens/BranchListScreen/BranchListView.tsx b/apps/expo-go/src/screens/BranchListScreen/BranchListView.tsx deleted file mode 100644 index 0af67a79a786db..00000000000000 --- a/apps/expo-go/src/screens/BranchListScreen/BranchListView.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { ApolloQueryResult } from '@apollo/client'; -import { spacing } from '@expo/styleguide-native'; -import { Divider, useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { FlatList, ActivityIndicator, View as RNView } from 'react-native'; - -import { BranchListItem } from '../../components/BranchListItem'; -import { CappedWidthContainerView } from '../../components/Views'; -import { BranchesForProjectQuery } from '../../graphql/types'; - -export type BranchManifest = { - name: string; - id: string; - latestUpdate: - | BranchesForProjectQuery['app']['byId']['updateBranches'][0]['updates'][0] - | undefined; -}; - -type Props = { - appId: string; - data: BranchManifest[]; - loadMoreAsync: () => Promise>; -}; - -export function BranchListView(props: Props) { - const [isReady, setReady] = React.useState(false); - - const theme = useExpoTheme(); - - React.useEffect(() => { - const _readyTimer = setTimeout(() => { - setReady(true); - }, 500); - return () => { - clearTimeout(_readyTimer); - }; - }, []); - - if (!isReady) { - return ( - - - - ); - } - - if (!props.data?.length) { - return ; - } - - return ; -} - -function BranchList({ data, appId, loadMoreAsync }: Props) { - const isLoading = React.useRef(false); - const theme = useExpoTheme(); - - const extractKey = (item: BranchManifest) => item.id; - - const handleLoadMoreAsync = async () => { - if (isLoading.current) return; - isLoading.current = true; - - try { - await loadMoreAsync(); - } catch (e) { - console.error(e); - } finally { - isLoading.current = false; - } - }; - - const renderItem = React.useCallback( - ({ item: branch, index }: { item: BranchManifest; index: number }) => { - return ( - - ); - }, - [appId, data] - ); - - return ( - - } - onEndReached={handleLoadMoreAsync} - onEndReachedThreshold={0.2} - /> - - ); -} diff --git a/apps/expo-go/src/screens/BranchListScreen/index.tsx b/apps/expo-go/src/screens/BranchListScreen/index.tsx deleted file mode 100644 index 6d44f0a7c359db..00000000000000 --- a/apps/expo-go/src/screens/BranchListScreen/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import * as React from 'react'; - -import { BranchList } from './BranchList'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -export function BranchListScreen({ route }: StackScreenProps) { - const { appId } = route.params ?? {}; - - return ; -} diff --git a/apps/expo-go/src/screens/DiagnosticsScreen/AudioDiagnosticsScreen.tsx b/apps/expo-go/src/screens/DiagnosticsScreen/AudioDiagnosticsScreen.tsx deleted file mode 100644 index 78b14e8ea003b4..00000000000000 --- a/apps/expo-go/src/screens/DiagnosticsScreen/AudioDiagnosticsScreen.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { InterruptionModeIOS } from 'expo-av'; -import React from 'react'; -import { StyleSheet, Switch, View } from 'react-native'; -import { BorderlessButton } from 'react-native-gesture-handler'; - -import AudioPlayer from '../../components/AudioPlayer'; -import { StyledText } from '../../components/Text'; -import { StyledScrollView } from '../../components/Views'; -import Colors from '../../constants/Colors'; -import Environment from '../../utils/Environment'; -import { useAudio, useAudioMode } from '../../utils/useAudio'; - -const initialAudioMode = { - interruptionModeIOS: InterruptionModeIOS.MixWithOthers, - playsInSilentModeIOS: false, - allowsRecordingIOS: false, - staysActiveInBackground: false, -}; - -export default function AudioDiagnosticsScreen() { - const [isAudioEnabled, setAudioEnabled] = useAudio(); - const [audioMode, setAudioMode] = useAudioMode(initialAudioMode); - - return ( - - Audio Player - - Audio Modes - { - setAudioEnabled(value); - }} - /> - { - const newAudioMode = { - ...audioMode, - playsInSilentModeIOS: value, - staysActiveInBackground: audioMode.staysActiveInBackground && value, - }; - setAudioMode(newAudioMode); - }} - /> - { - const newAudioMode = { ...audioMode, allowsRecordingIOS: value }; - setAudioMode(newAudioMode); - }} - /> - {!Environment.IsIOSRestrictedBuild ? ( - { - const newAudioMode = { ...audioMode, staysActiveInBackground: value }; - setAudioMode(newAudioMode); - }} - /> - ) : null} - { - const newAudioMode = { ...audioMode, interruptionModeIOS: value }; - setAudioMode(newAudioMode); - }} - /> - - ); -} - -type AudioOptionSwitchProps = { - title: string; - disabled?: boolean; - value?: boolean; - onValueChange: (value: boolean) => void; -}; - -function AudioOptionSwitch(props: AudioOptionSwitchProps) { - return ( - - {props.title} - - - ); -} - -type AudioOptionSelectorProps = { - title: string; - disabled?: boolean; - items: { name: string; value: T; disabled?: boolean }[]; - selectedValue: T; - onSelect: (value: T) => void; -}; - -function AudioOptionSelector(props: AudioOptionSelectorProps) { - return ( - <> - {props.title} - {props.items.map((item) => ( - props.onSelect(item.value)} - style={styles.selectorButton}> - - {item.name} - {Object.is(item.value, props.selectedValue) ? ' โœ“' : null} - - - ))} - - - ); -} - -const styles = StyleSheet.create({ - screen: { - flexGrow: 1, - }, - contentContainer: { - paddingVertical: 16, - }, - title: { - fontSize: 17, - fontWeight: 'bold', - paddingHorizontal: 8, - }, - switch: { - alignItems: 'center', - borderBottomColor: Colors.light.navBorderBottom, - borderBottomWidth: StyleSheet.hairlineWidth, - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: 8, - paddingVertical: 5, - }, - optionTitle: { - flex: 1, - fontSize: 16, - }, - selectorTitle: { - fontSize: 16, - paddingBottom: 5, - paddingHorizontal: 8, - paddingTop: 10, - }, - selectorButton: { - paddingHorizontal: 8, - paddingVertical: 3, - }, - selectorButtonStyledText: { - color: Colors.light.tintColor, - fontSize: 16, - padding: 5, - }, - disabledSelectorButtonStyledText: { - color: Colors.light.greyText, - }, -}); diff --git a/apps/expo-go/src/screens/DiagnosticsScreen/DiagnosticsButton.tsx b/apps/expo-go/src/screens/DiagnosticsScreen/DiagnosticsButton.tsx deleted file mode 100644 index dfd7ea8f0a868a..00000000000000 --- a/apps/expo-go/src/screens/DiagnosticsScreen/DiagnosticsButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { - ChevronRightIcon, - Row, - Spacer, - Text, - useExpoTheme, - View, -} from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -type Props = { - title: string; - description: string; - onPress: () => void; -}; - -export function DiagnosticButton({ title, description, onPress }: Props) { - const theme = useExpoTheme(); - - return ( - - - - - {title} - - - - - - {description} - - - - ); -} diff --git a/apps/expo-go/src/screens/DiagnosticsScreen/GeofencingDiagnosticsScreen.tsx b/apps/expo-go/src/screens/DiagnosticsScreen/GeofencingDiagnosticsScreen.tsx deleted file mode 100644 index f29c74483fc072..00000000000000 --- a/apps/expo-go/src/screens/DiagnosticsScreen/GeofencingDiagnosticsScreen.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import MaterialIcons from '@expo/vector-icons/build/MaterialIcons'; -import * as Location from 'expo-location'; -import * as Notifications from 'expo-notifications'; -import * as TaskManager from 'expo-task-manager'; -import * as React from 'react'; -import { - AppState, - AppStateStatus, - NativeEventSubscription, - Platform, - StyleSheet, - View, -} from 'react-native'; -import MapView, { Circle, MapPressEvent } from 'react-native-maps'; - -import NavigationEvents from '../../components/NavigationEvents'; -import Button from '../../components/PrimaryButton'; -import { StyledText } from '../../components/Text'; - -const GEOFENCING_TASK = 'geofencing'; -const REGION_RADIUSES = [30, 50, 75, 100, 150, 200]; - -type Region = { - identifier: string; - latitude: number; - longitude: number; - radius: number; -}; - -type State = { - isGeofencing: boolean; - newRegionRadius: number; - geofencingRegions: Region[]; - initialRegion: { - latitude: number; - longitude: number; - latitudeDelta: number; - longitudeDelta: number; - } | null; - error: string | null; -}; - -type Props = unknown; - -export default class GeofencingScreen extends React.Component { - mapViewRef = React.createRef(); - - appStateSubscription?: NativeEventSubscription; - - readonly state: State = { - isGeofencing: false, - newRegionRadius: REGION_RADIUSES[1], - geofencingRegions: [], - initialRegion: null, - error: null, - }; - - didFocus = async () => { - const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync(); - - if (foregroundStatus !== 'granted') { - this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange); - this.setState({ - error: - 'Location permissions are required in order to use this feature. You can manually enable them at any time in the "Location Services" section of the Settings app.', - }); - return; - } else { - this.setState({ error: null }); - } - - const { coords } = await Location.getCurrentPositionAsync(); - const isGeofencing = await Location.hasStartedGeofencingAsync(GEOFENCING_TASK); - const geofencingRegions = await getSavedRegions(); - - if (!isGeofencing) { - alert( - 'Tap on the map to select a region with chosen radius and then press `Start geofencing` to start getting geofencing notifications.' - ); - } - - this.setState({ - isGeofencing, - geofencingRegions, - initialRegion: { - latitude: coords.latitude, - longitude: coords.longitude, - latitudeDelta: 0.004, - longitudeDelta: 0.002, - }, - }); - }; - - handleAppStateChange = (nextAppState: AppStateStatus) => { - if (nextAppState !== 'active') { - return; - } - - if (this.state.initialRegion) { - if (this.appStateSubscription != null) { - this.appStateSubscription.remove(); - this.appStateSubscription = undefined; - } - return; - } - - this.didFocus(); - }; - - canToggleGeofencing() { - return this.state.isGeofencing || this.state.geofencingRegions.length > 0; - } - - toggleGeofencing = async () => { - if (!this.canToggleGeofencing()) { - return; - } - - if (this.state.isGeofencing) { - await Location.stopGeofencingAsync(GEOFENCING_TASK); - this.setState({ geofencingRegions: [] }); - } else { - await Location.startGeofencingAsync(GEOFENCING_TASK, this.state.geofencingRegions); - alert( - 'You will be receiving notifications when the device enters or exits from selected regions.' - ); - } - this.setState((state) => ({ isGeofencing: !state.isGeofencing })); - }; - - shiftRegionRadius = () => { - const index = REGION_RADIUSES.indexOf(this.state.newRegionRadius) + 1; - const radius = index < REGION_RADIUSES.length ? REGION_RADIUSES[index] : REGION_RADIUSES[0]; - - this.setState({ newRegionRadius: radius }); - }; - - centerMap = async () => { - const { coords } = await Location.getCurrentPositionAsync(); - const mapView = this.mapViewRef.current; - - if (mapView) { - mapView.animateToRegion({ - latitude: coords.latitude, - longitude: coords.longitude, - latitudeDelta: 0.004, - longitudeDelta: 0.002, - }); - } - }; - - onMapPress = ({ nativeEvent: { coordinate } }: MapPressEvent) => { - this.setState( - (state) => ({ - geofencingRegions: [ - ...state.geofencingRegions, - { - identifier: `${coordinate.latitude},${coordinate.longitude}`, - latitude: coordinate.latitude, - longitude: coordinate.longitude, - radius: state.newRegionRadius, - }, - ], - }), - async () => { - if (await Location.hasStartedGeofencingAsync(GEOFENCING_TASK)) { - // update existing geofencing task - await Location.startGeofencingAsync(GEOFENCING_TASK, this.state.geofencingRegions); - } - } - ); - }; - - renderRegions() { - const { geofencingRegions } = this.state; - - return geofencingRegions.map((region) => { - return ( - - ); - }); - } - - getGeofencingButtonContent() { - const canToggle = this.canToggleGeofencing(); - - if (canToggle) { - return this.state.isGeofencing ? 'Stop geofencing' : 'Start geofencing'; - } - return 'Select at least one region on the map'; - } - - render() { - if (this.state.error) { - return {this.state.error}; - } - - if (!this.state.initialRegion) { - return ; - } - - const canToggle = this.canToggleGeofencing(); - - return ( - - - - - - - - - - - - - - - - - ); - } -} - -async function getSavedRegions() { - const tasks = await TaskManager.getRegisteredTasksAsync(); - const task = tasks.find(({ taskName }) => taskName === GEOFENCING_TASK); - return task ? task.options.regions : []; -} - -if (Platform.OS !== 'android') { - TaskManager.defineTask(GEOFENCING_TASK, async ({ data: { region } }: any) => { - const stateString = Location.GeofencingRegionState[region.state].toLowerCase(); - const body = `You're ${stateString} a region with latitude: ${region.latitude}, longitude: ${region.longitude} and radius: ${region.radius}m`; - await Notifications.scheduleNotificationAsync({ - content: { - title: 'Expo Geofencing', - body, - data: { - ...region, - notificationBody: body, - notificationType: GEOFENCING_TASK, - }, - }, - trigger: null, - }); - }); -} - -Notifications.addNotificationResponseReceivedListener((response) => { - if (response.notification.request.content.data?.notificationType === GEOFENCING_TASK) { - alert(response.notification.request.content.data.notificationBody); - } -}); -Notifications.addNotificationReceivedListener((notification) => { - if (notification.request.content.data?.notificationType === GEOFENCING_TASK) { - alert(notification.request.content.data.notificationBody); - } -}); - -const styles = StyleSheet.create({ - screen: { - flex: 1, - }, - mapView: { - flex: 1, - }, - buttons: { - flex: 1, - flexDirection: 'column', - justifyContent: 'space-between', - padding: 10, - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - }, - topButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - bottomButtons: { - flexDirection: 'column', - alignItems: 'flex-end', - }, - buttonsColumn: { - flexDirection: 'column', - alignItems: 'flex-start', - }, - button: { - paddingVertical: 5, - paddingHorizontal: 10, - marginVertical: 5, - }, - disabledButton: { - backgroundColor: 'gray', - opacity: 0.8, - }, - errorText: { - fontSize: 15, - color: 'rgba(0,0,0,0.7)', - margin: 20, - }, -}); diff --git a/apps/expo-go/src/screens/DiagnosticsScreen/LocationDiagnosticsScreen.tsx b/apps/expo-go/src/screens/DiagnosticsScreen/LocationDiagnosticsScreen.tsx deleted file mode 100644 index 6fabfe3cedaa85..00000000000000 --- a/apps/expo-go/src/screens/DiagnosticsScreen/LocationDiagnosticsScreen.tsx +++ /dev/null @@ -1,353 +0,0 @@ -import FontAwesome from '@expo/vector-icons/build/FontAwesome'; -import MaterialIcons from '@expo/vector-icons/build/MaterialIcons'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import * as Location from 'expo-location'; -import * as TaskManager from 'expo-task-manager'; -import { EventEmitter, EventSubscription } from 'fbemitter'; -import * as React from 'react'; -import { - AppState, - AppStateStatus, - NativeEventSubscription, - Platform, - StyleSheet, - Text, - View, -} from 'react-native'; -import MapView, { Polyline } from 'react-native-maps'; - -import NavigationEvents from '../../components/NavigationEvents'; -import Button from '../../components/PrimaryButton'; -import { StyledText } from '../../components/Text'; -import Colors from '../../constants/Colors'; - -const STORAGE_KEY = 'expo-home-locations'; -const LOCATION_UPDATES_TASK = 'location-updates'; - -const locationEventsEmitter = new EventEmitter(); - -type Region = { - identifier: string; - latitude: number; - longitude: number; - radius: number; -}; - -type State = { - isBackgroundLocationAvailable: null | boolean; - accuracy: Location.Accuracy; - isTracking: boolean; - showsBackgroundLocationIndicator: boolean; - savedLocations: Region[]; - initialRegion: { - latitude: number; - longitude: number; - latitudeDelta: number; - longitudeDelta: number; - } | null; - error: string | null; -}; - -type Props = unknown; - -export default class LocationDiagnosticsScreen extends React.Component { - mapViewRef = React.createRef(); - - eventSubscription?: EventSubscription; - appStateSubscription?: NativeEventSubscription; - - readonly state: State = { - isBackgroundLocationAvailable: null, - accuracy: Location.Accuracy.High, - isTracking: false, - showsBackgroundLocationIndicator: false, - savedLocations: [], - initialRegion: null, - error: null, - }; - - componentDidMount() { - this.checkBackgroundLocationAvailability(); - } - - async checkBackgroundLocationAvailability() { - const isBackgroundLocationAvailable = await Location.isBackgroundLocationAvailableAsync(); - this.setState({ isBackgroundLocationAvailable }); - } - - didFocus = async () => { - const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync(); - - if (foregroundStatus !== 'granted') { - this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange); - this.setState({ - error: - 'Location access is required to be set to `Always` in order to use this feature. You can manually enable them at any time in the "Location Services" section of the Settings app.', - }); - return; - } - - const { coords } = await Location.getCurrentPositionAsync(); - const isTracking = await Location.hasStartedLocationUpdatesAsync(LOCATION_UPDATES_TASK); - const task = (await TaskManager.getRegisteredTasksAsync()).find( - ({ taskName }) => taskName === LOCATION_UPDATES_TASK - ); - const savedLocations = await getSavedLocations(); - - this.eventSubscription = locationEventsEmitter.addListener('update', (locations: Region[]) => { - this.setState({ savedLocations: locations }); - }); - - if (!isTracking) { - alert('Click `Start tracking` to start getting location updates.'); - } - - this.setState((state) => ({ - accuracy: (task && task.options.accuracy) || state.accuracy, - isTracking, - savedLocations, - initialRegion: { - latitude: coords.latitude, - longitude: coords.longitude, - latitudeDelta: 0.004, - longitudeDelta: 0.002, - }, - error: null, - })); - }; - - handleAppStateChange = (nextAppState: AppStateStatus) => { - if (nextAppState !== 'active') { - return; - } - - if (this.state.initialRegion) { - if (this.appStateSubscription) { - this.appStateSubscription.remove(); - this.appStateSubscription = undefined; - } - return; - } - - this.didFocus(); - }; - - componentWillUnmount() { - if (this.eventSubscription) { - this.eventSubscription.remove(); - } - - if (this.appStateSubscription != null) { - this.appStateSubscription.remove(); - this.appStateSubscription = undefined; - } - } - - async startLocationUpdates(accuracy = this.state.accuracy) { - await Location.startLocationUpdatesAsync(LOCATION_UPDATES_TASK, { - accuracy, - showsBackgroundLocationIndicator: this.state.showsBackgroundLocationIndicator, - }); - - if (!this.state.isTracking) { - alert( - 'Now you can send app to the background, go somewhere and come back here! You can even terminate the app and it will be woken up when the new significant location change comes out.' - ); - } - this.setState({ isTracking: true }); - } - - async stopLocationUpdates() { - await Location.stopLocationUpdatesAsync(LOCATION_UPDATES_TASK); - this.setState({ isTracking: false }); - } - - clearLocations = async () => { - await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([])); - this.setState({ savedLocations: [] }); - }; - - toggleTracking = async () => { - await AsyncStorage.removeItem(STORAGE_KEY); - - if (this.state.isTracking) { - await this.stopLocationUpdates(); - } else { - await this.startLocationUpdates(); - } - this.setState({ savedLocations: [] }); - }; - - onAccuracyChange = () => { - const next = Location.Accuracy[this.state.accuracy + 1]; - const accuracy = next - ? (Location.Accuracy[next as any] as any as Location.Accuracy) - : Location.Accuracy.Lowest; - - this.setState({ accuracy }); - - if (this.state.isTracking) { - // Restart background task with the new accuracy. - this.startLocationUpdates(accuracy); - } - }; - - toggleLocationIndicator = async () => { - this.setState( - (state) => ({ showsBackgroundLocationIndicator: !state.showsBackgroundLocationIndicator }), - async () => { - if (this.state.isTracking) { - await this.startLocationUpdates(); - } - } - ); - }; - - onCenterMap = async () => { - const { coords } = await Location.getCurrentPositionAsync(); - const mapView = this.mapViewRef.current; - - if (mapView) { - mapView.animateToRegion({ - latitude: coords.latitude, - longitude: coords.longitude, - latitudeDelta: 0.004, - longitudeDelta: 0.002, - }); - } - }; - - renderPolyline() { - const { savedLocations } = this.state; - - if (savedLocations.length === 0) { - return null; - } - - return ( - - ); - } - - render() { - if (this.state.error) { - return {this.state.error}; - } - - if (!this.state.initialRegion) { - return ; - } - - return ( - - - {this.renderPolyline()} - - - - - {this.state.isBackgroundLocationAvailable && ( - - )} - - - - - - - - - - {this.state.isBackgroundLocationAvailable && ( - - )} - - - - ); - } -} - -async function getSavedLocations() { - try { - const item = await AsyncStorage.getItem(STORAGE_KEY); - return item ? JSON.parse(item) : []; - } catch { - return []; - } -} - -if (Platform.OS !== 'android') { - TaskManager.defineTask(LOCATION_UPDATES_TASK, async ({ data: { locations } }: any) => { - if (locations && locations.length > 0) { - const savedLocations = await getSavedLocations(); - const newLocations = locations.map(({ coords }: { coords: any }) => ({ - latitude: coords.latitude, - longitude: coords.longitude, - })); - - savedLocations.push(...newLocations); - await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(savedLocations)); - - locationEventsEmitter.emit('update', savedLocations); - } - }); -} - -const styles = StyleSheet.create({ - screen: { - flex: 1, - }, - mapView: { - flex: 1, - }, - buttons: { - flex: 1, - flexDirection: 'column', - justifyContent: 'space-between', - padding: 10, - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0, - }, - topButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - bottomButtons: { - flexDirection: 'column', - alignItems: 'flex-end', - }, - buttonsColumn: { - flexDirection: 'column', - alignItems: 'flex-start', - }, - button: { - paddingVertical: 5, - paddingHorizontal: 10, - marginVertical: 5, - }, - errorText: { - fontSize: 15, - margin: 20, - }, -}); diff --git a/apps/expo-go/src/screens/DiagnosticsScreen/index.android.tsx b/apps/expo-go/src/screens/DiagnosticsScreen/index.android.tsx deleted file mode 100644 index d3a55d81c8aef9..00000000000000 --- a/apps/expo-go/src/screens/DiagnosticsScreen/index.android.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function DiagnosticsStackScreen() { - return null; -} diff --git a/apps/expo-go/src/screens/DiagnosticsScreen/index.tsx b/apps/expo-go/src/screens/DiagnosticsScreen/index.tsx deleted file mode 100644 index 379f68f04d38df..00000000000000 --- a/apps/expo-go/src/screens/DiagnosticsScreen/index.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { useTheme } from '@react-navigation/native'; -import { - createStackNavigator, - StackNavigationProp, - StackScreenProps, -} from '@react-navigation/stack'; -import { Spacer } from 'expo-dev-client-components'; -import * as React from 'react'; - -import AudioDiagnosticsScreen from './AudioDiagnosticsScreen'; -import { DiagnosticButton } from './DiagnosticsButton'; -import GeofencingScreen from './GeofencingDiagnosticsScreen'; -import LocationDiagnosticsScreen from './LocationDiagnosticsScreen'; -import ScrollView from '../../components/NavigationScrollView'; -import { CappedWidthContainerView } from '../../components/Views'; -import { ColorTheme } from '../../constants/Colors'; -import { DiagnosticsStackRoutes } from '../../navigation/Navigation.types'; -import defaultNavigationOptions from '../../navigation/defaultNavigationOptions'; -import Environment from '../../utils/Environment'; - -function useThemeName() { - const theme = useTheme(); - return theme.dark ? ColorTheme.DARK : ColorTheme.LIGHT; -} - -const DiagnosticsStack = createStackNavigator(); - -export function DiagnosticsStackScreen() { - const theme = useThemeName(); - - return ( - - <>, - }} - /> - - - - - ); -} - -function DiagnosticsScreen({ - navigation, -}: StackScreenProps) { - return ( - - - - - - {Environment.IsIOSRestrictedBuild ? ( - - ) : ( - - )} - - - - - ); -} - -function AudioDiagnostic({ - navigation, -}: { - navigation: StackNavigationProp; -}) { - return ( - navigation.navigate('Audio', {})} - /> - ); -} - -function BackgroundLocationDiagnostic({ - navigation, -}: { - navigation: StackNavigationProp; -}) { - return ( - navigation.navigate('Location', {})} - /> - ); -} - -function ForegroundLocationDiagnostic({ - navigation, -}: { - navigation: StackNavigationProp; -}) { - return ( - navigation.navigate('Location', {})} - /> - ); -} - -function GeofencingDiagnostic({ - navigation, -}: { - navigation: StackNavigationProp; -}) { - return ( - navigation.navigate('Geofencing', {})} - /> - ); -} diff --git a/apps/expo-go/src/screens/FeedbackFormScreen/index.tsx b/apps/expo-go/src/screens/FeedbackFormScreen/index.tsx deleted file mode 100644 index 79092383dd0ffa..00000000000000 --- a/apps/expo-go/src/screens/FeedbackFormScreen/index.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { CheckIcon, spacing } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import * as Application from 'expo-application'; -import { - Text, - View, - TextInput, - Button, - Heading, - Spacer, - useExpoTheme, -} from 'expo-dev-client-components'; -import * as Device from 'expo-device'; -import * as React from 'react'; -import { useState } from 'react'; -import { ActivityIndicator, ScrollView, StyleSheet } from 'react-native'; - -import { APIV2Client } from '../../api/APIV2Client'; -import { useInitialData } from '../../utils/InitialDataContext'; - -const EMAIL_REGEX = - /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i; - -export function FeedbackFormScreen() { - const theme = useExpoTheme(); - const navigation = useNavigation(); - const { currentUserData } = useInitialData(); - - const [submitting, setSubmitting] = useState(false); - const [submitted, setSubmitted] = useState(false); - const [error, setError] = useState(); - - const [feedback, setFeedback] = useState(''); - const [email, setEmail] = useState(currentUserData?.meUserActor?.bestContactEmail || ''); - - async function onSubmit() { - setError(undefined); - setSubmitting(true); - try { - const body = { - feedback, - email: undefined as string | undefined, - metadata: { - os: `${Device.osName} ${Device.osVersion}`, - model: Device.modelName, - expoGoVersion: Application.nativeApplicationVersion, - }, - }; - if (email.trim().length > 0) { - if (!EMAIL_REGEX.test(email)) { - setError('Please enter a valid email address.'); - return; - } - body.email = email; - } - const api = new APIV2Client(); - await api.sendUnauthenticatedApiV2Request('/feedback/expo-go-send', { - body, - }); - setSubmitted(true); - } catch (error: any) { - setError(error.message); - } finally { - setSubmitting(false); - } - } - - if (submitted) { - return ( - - - - - Thanks for sharing your feedback! - - Your feedback will help us make our app better. - - - - Continue - - - - ); - } - - return ( - - - - - Add your feedback to help us improve this our app. - - - Email (optional) - - - - - - - Feedback - - - - - - - - {error ? ( - - - Something went wrong. Please try again. - - - {error} - - - ) : null} - - {submitting ? ( - - ) : ( - - Submit - - )} - - - - ); -} - -const styles = StyleSheet.create({ - activityIndicator: { - height: 26, - }, - thanksForSharingContainer: { - alignItems: 'center', - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/AppIcon.tsx b/apps/expo-go/src/screens/HomeScreen/AppIcon.tsx deleted file mode 100644 index 3ad54171cabc8e..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/AppIcon.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { View } from 'expo-dev-client-components'; -import * as React from 'react'; -import { StyleSheet, Image } from 'react-native'; -import FadeIn from 'react-native-fade-in-image'; - -type Props = { - image?: number | string | null; -}; - -export function AppIcon(props: Props) { - const { image } = props; - - if (image !== undefined) { - if (image === null) { - return ; - } else { - const source = typeof image === 'number' ? image : { uri: image }; - return ( - - - - - - ); - } - } else { - return null; - } -} - -const styles = StyleSheet.create({ - imageContainer: { - borderRadius: 8, - justifyContent: 'center', - alignItems: 'center', - marginEnd: spacing[2], - }, - image: { - borderRadius: 8, - }, - emptyImage: { - backgroundColor: '#eee', - }, - iconContainer: { - alignSelf: 'center', - marginEnd: 10, - alignItems: 'center', - justifyContent: 'center', - }, - icon: { - fontSize: 28, - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/DevelopmentServerSubtitle.tsx b/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/DevelopmentServerSubtitle.tsx deleted file mode 100644 index 5e6d3301f9513e..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/DevelopmentServerSubtitle.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Text } from 'expo-dev-client-components'; -import * as React from 'react'; -import { StyleSheet } from 'react-native'; - -type DevelopmentServerSubtitleProps = { - title?: string; - subtitle?: string; - onPressSubtitle?: () => any; - image?: number | string | null; -}; - -export function DevelopmentServerSubtitle({ - title, - subtitle, - image, - onPressSubtitle, -}: DevelopmentServerSubtitleProps) { - const isCentered = !title && !image; - - return subtitle ? ( - - {subtitle} - - ) : null; -} - -const styles = StyleSheet.create({ - subtitleMarginBottom: { - marginBottom: 2, - }, - subtitleCentered: { - textAlign: 'center', - marginEnd: 10, - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/DevelopmentServerTitle.tsx b/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/DevelopmentServerTitle.tsx deleted file mode 100644 index e47933f8488e56..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/DevelopmentServerTitle.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Text } from 'expo-dev-client-components'; -import * as React from 'react'; -import { View as RNView, StyleSheet } from 'react-native'; - -import PlatformIcon from '../../../components/PlatformIcon'; - -type PlatformIconProps = React.ComponentProps; - -type DevelopmentServerTitleProps = { - title?: string; - platform?: PlatformIconProps['platform']; -}; - -export function DevelopmentServerTitle({ title, platform }: DevelopmentServerTitleProps) { - return title ? ( - - {platform && } - - {title} - - - ) : null; -} - -const styles = StyleSheet.create({ - titleContainer: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 2, - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/index.tsx b/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/index.tsx deleted file mode 100644 index edcf5dbbf557c9..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/DevelopmentServerListItem/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { ChevronDownIcon, spacing } from '@expo/styleguide-native'; -import { NavigationProp, useNavigation } from '@react-navigation/native'; -import { useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { View as RNView, StyleSheet, ViewStyle, Share, Linking } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { DevelopmentServerSubtitle } from './DevelopmentServerSubtitle'; -import { DevelopmentServerTitle } from './DevelopmentServerTitle'; -import PlatformIcon from '../../../components/PlatformIcon'; -import { HomeStackRoutes } from '../../../navigation/Navigation.types'; -import * as UrlUtils from '../../../utils/UrlUtils'; -import { AppIcon } from '../AppIcon'; - -type Props = { - style?: ViewStyle; - imageSize?: number; - onPress?: () => any; - onLongPress?: () => any; - disabled?: boolean; - title?: string; - subtitle?: string; - onPressSubtitle?: () => any; - renderExtraText?: () => any; - margins?: boolean; - image?: number | string | null; - imageStyle?: ViewStyle; - arrowForward?: boolean; - rightContent?: React.ReactNode; - platform?: PlatformIconProps['platform']; - url: string; - username?: string; - experienceInfo?: { - id: string; - username: string; - slug: string; - }; -}; - -type PlatformIconProps = React.ComponentProps; - -export function DevelopmentServerListItem({ - username, - subtitle, - title, - url, - image, - experienceInfo, - disabled, - platform, - style, - onPressSubtitle, -}: Props) { - const theme = useExpoTheme(); - const navigation = useNavigation>(); - - const handlePress = () => { - if (experienceInfo) { - navigation.navigate('Project', { id: experienceInfo.id }); - } else if (url) { - Linking.openURL(UrlUtils.normalizeUrl(url)); - } - }; - - const handleLongPress = () => { - const message = UrlUtils.normalizeUrl(url); - Share.share({ - title: url, - message, - url: message, - }); - }; - - return ( - - - - - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - padding: spacing[4], - justifyContent: 'space-between', - }, - disabled: { - opacity: 0.5, - }, - pressed: { - opacity: 0.8, - }, - contentContainer: { - backgroundColor: 'transparent', - flex: 1, - flexDirection: 'row', - }, - textContainer: { - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'flex-start', - }, - chevronRightContainer: { - alignSelf: 'center', - marginStart: spacing[2], - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersHeader.tsx b/apps/expo-go/src/screens/HomeScreen/DevelopmentServersHeader.tsx deleted file mode 100644 index 80d39e41c2fafd..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersHeader.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { Heading, Row, TerminalIcon, View, Text } from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -type DevelopmentServersHeaderProps = { - onHelpPress: () => void; -}; - -export function DevelopmentServersHeader({ onHelpPress }: DevelopmentServersHeaderProps) { - return ( - - - - - - - Development servers - - - - - HELP - - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersOpenQR.tsx b/apps/expo-go/src/screens/HomeScreen/DevelopmentServersOpenQR.tsx deleted file mode 100644 index e8dc0990be7187..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersOpenQR.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { iconSize, QrCodeIcon, spacing } from '@expo/styleguide-native'; -import { NavigationProp, useNavigation } from '@react-navigation/native'; -import { Divider, Row, Text, useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { ModalStackRoutes } from '../../navigation/Navigation.types'; -import { - alertWithCameraPermissionInstructions, - requestCameraPermissionsAsync, -} from '../../utils/PermissionUtils'; - -export function DevelopmentServersOpenQR() { - const theme = useExpoTheme(); - - const navigation = useNavigation>(); - - const handleQRPressAsync = async () => { - if (await requestCameraPermissionsAsync()) { - navigation.navigate('QRCode'); - } else { - await alertWithCameraPermissionInstructions(); - } - }; - - return ( - <> - - - - - Scan QR code - - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersOpenURL.tsx b/apps/expo-go/src/screens/HomeScreen/DevelopmentServersOpenURL.tsx deleted file mode 100644 index 3da9f3809fab10..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersOpenURL.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { borderRadius, spacing } from '@expo/styleguide-native'; -import { - Button, - ChevronRightIcon, - Divider, - Row, - Spacer, - Text, - TextInput, - useExpoTheme, - View, -} from 'expo-dev-client-components'; -import * as React from 'react'; -import { Animated, Linking } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import * as UrlUtils from '../../utils/UrlUtils'; - -export function DevelopmentServersOpenURL() { - const [showInput, setShowInput] = React.useState(false); - const [url, setUrl] = React.useState(''); - - const theme = useExpoTheme(); - const rotateAnimation = React.useRef(new Animated.Value(0)).current; - - const interpolateRotating = rotateAnimation.interpolate({ - inputRange: [0, 1], - outputRange: ['0deg', '90deg'], - }); - - React.useEffect( - function animateChevron() { - Animated.timing(rotateAnimation, { - toValue: showInput ? 1 : 0, - duration: 100, - useNativeDriver: false, - }).start(() => { - rotateAnimation.setValue(showInput ? 1 : 0); - }); - }, - [showInput] - ); - - function openURL() { - if (url) { - const normalizedUrl = UrlUtils.normalizeUrl(url); - Linking.openURL(normalizedUrl); - } - } - - return ( - <> - - - setShowInput((prevState) => !prevState)}> - - - - - Enter URL manually - - - {showInput && } - {showInput && ( - - setUrl(newUrl.trim())} - border="default" - rounded="medium" - shadow="input" - autoCorrect={false} - autoComplete="off" - autoCapitalize="none" - returnKeyType="done" - onSubmitEditing={openURL} - style={{ backgroundColor: theme.background.default }} - px="4" - py="3" - type="InterRegular" - placeholder="exp://" - placeholderTextColor={theme.text.secondary} - /> - - - - Connect - - - - )} - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersPlaceholder.tsx b/apps/expo-go/src/screens/HomeScreen/DevelopmentServersPlaceholder.tsx deleted file mode 100644 index 47f952987314f7..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/DevelopmentServersPlaceholder.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { NavigationProp, useNavigation } from '@react-navigation/native'; -import { Text, View } from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { DevelopmentServersOpenQR } from './DevelopmentServersOpenQR'; -import { DevelopmentServersOpenURL } from './DevelopmentServersOpenURL'; -import FeatureFlags from '../../FeatureFlags'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -type Props = { - isAuthenticated: boolean; -}; - -export function DevelopmentServersPlaceholder({ isAuthenticated }: Props) { - const navigation = useNavigation>(); - - return isAuthenticated ? ( - - - - Start a local development server with: - - - - npx expo start - - - - Select the local server when it appears here. - - - {FeatureFlags.ENABLE_PROJECT_TOOLS && FeatureFlags.ENABLE_CLIPBOARD_BUTTON ? ( - - ) : null} - {FeatureFlags.ENABLE_PROJECT_TOOLS && FeatureFlags.ENABLE_QR_CODE_BUTTON ? ( - - ) : null} - - ) : ( - navigation.navigate('Account')} - hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> - - - - Press here to sign in to your Expo account and see the projects you have recently been - working on. - - - {FeatureFlags.ENABLE_PROJECT_TOOLS && FeatureFlags.ENABLE_CLIPBOARD_BUTTON ? ( - - ) : null} - {FeatureFlags.ENABLE_PROJECT_TOOLS && FeatureFlags.ENABLE_QR_CODE_BUTTON ? ( - - ) : null} - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/HomeScreenData.query.graphql b/apps/expo-go/src/screens/HomeScreen/HomeScreenData.query.graphql deleted file mode 100644 index f85bd28eea6163..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/HomeScreenData.query.graphql +++ /dev/null @@ -1,18 +0,0 @@ -query HomeScreenData($accountName: String!, $platform: AppPlatform!) { - account { - byName(accountName: $accountName) { - id - name - ownerUserActor { - ...CurrentUserActorData - } - apps(limit: 5, offset: 0, includeUnpublished: true) { - ...CommonAppData - } - snacks(limit: 5, offset: 0) { - ...CommonSnackData - } - appCount - } - } -} diff --git a/apps/expo-go/src/screens/HomeScreen/HomeScreenHeader.tsx b/apps/expo-go/src/screens/HomeScreen/HomeScreenHeader.tsx deleted file mode 100644 index 85ee208c02fd47..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/HomeScreenHeader.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { borderRadius, iconSize, spacing, UsersIcon } from '@expo/styleguide-native'; -import { NavigationProp, useNavigation } from '@react-navigation/native'; -import { Button, View, Row, Image, Text } from 'expo-dev-client-components'; -import * as Haptics from 'expo-haptics'; -import * as React from 'react'; -import { StyleSheet } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { CappedWidthContainerView } from '../../components/Views'; -import { HomeScreenDataQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; -import { useTheme } from '../../utils/useTheme'; - -type Props = { - currentAccount?: Exclude; -}; - -export function HomeScreenHeader({ currentAccount }: Props) { - const { theme, themeType } = useTheme(); - const navigation = useNavigation>(); - - async function onAccountButtonPress() { - Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - navigation.navigate('Account'); - } - - let rightContent: React.ReactNode | null = null; - - if (currentAccount) { - rightContent = ( - - {/* Show profile picture for personal accounts / accounts with members */} - {currentAccount?.ownerUserActor?.profilePhoto ? ( - - ) : ( - - - - )} - - ); - } else { - // when user is logged out, show log in button - rightContent = ( - - - Log In - - - ); - } - - return ( - - - - - - - - Expo Go - - - {rightContent} - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/HomeScreenView.tsx b/apps/expo-go/src/screens/HomeScreen/HomeScreenView.tsx deleted file mode 100644 index b3ca1343980e80..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/HomeScreenView.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { StackScreenProps } from '@react-navigation/stack'; -import { View, Divider, Spacer } from 'expo-dev-client-components'; -import { isDevice } from 'expo-device'; -import * as React from 'react'; -import { - Alert, - AppState, - NativeEventSubscription, - Platform, - StyleSheet, - RefreshControl, -} from 'react-native'; - -import { DevelopmentServerListItem } from './DevelopmentServerListItem'; -import { DevelopmentServersHeader } from './DevelopmentServersHeader'; -import { DevelopmentServersOpenQR } from './DevelopmentServersOpenQR'; -import { DevelopmentServersOpenURL } from './DevelopmentServersOpenURL'; -import { DevelopmentServersPlaceholder } from './DevelopmentServersPlaceholder'; -import { HomeScreenHeader } from './HomeScreenHeader'; -import { ProjectsSection } from './ProjectsSection'; -import { RecentlyOpenedHeader } from './RecentlyOpenedHeader'; -import { RecentlyOpenedSection } from './RecentlyOpenedSection'; -import { SnacksSection } from './SnacksSection'; -import { UpgradeWarning } from './UpgradeWarning'; -import FeatureFlags from '../../FeatureFlags'; -import { APIV2Client } from '../../api/APIV2Client'; -import ApolloClient from '../../api/ApolloClient'; -import Connectivity from '../../api/Connectivity'; -import ScrollView from '../../components/NavigationScrollView'; -import { SectionHeader } from '../../components/SectionHeader'; -import ThemedStatusBar from '../../components/ThemedStatusBar'; -import UserReviewSection from '../../components/UserReviewSection'; -import { CappedWidthContainerView } from '../../components/Views'; -import { - AppPlatform, - HomeScreenDataDocument, - HomeScreenDataQuery, - HomeScreenDataQueryVariables, -} from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; -import HistoryActions from '../../redux/HistoryActions'; -import { DevSession, HistoryList } from '../../types'; -import addListenerWithNativeCallback from '../../utils/addListenerWithNativeCallback'; - -const PROJECT_UPDATE_INTERVAL = 10000; - -type Props = NavigationProps & { - dispatch: (data: any) => any; - isFocused: boolean; - recentHistory: HistoryList; - allHistory: HistoryList; - isAuthenticated: boolean; - theme: string; - accountName?: string; - initialData?: HomeScreenDataQuery; -}; - -type State = { - projects: DevSession[]; - isNetworkAvailable: boolean; - isRefreshing: boolean; - data?: Exclude; -}; - -type NavigationProps = StackScreenProps; - -export class HomeScreenView extends React.Component { - private _projectPolling?: ReturnType; - private _changeEventListener?: NativeEventSubscription; - - state: State = { - projects: [], - isNetworkAvailable: Connectivity.isAvailable(), - isRefreshing: false, - data: this.props.initialData?.account.byName, - }; - - componentDidMount() { - AppState.addEventListener('change', this._maybeResumePollingFromAppState); - Connectivity.addListener(this._updateConnectivity); - - // @evanbacon: Without this setTimeout, the state doesn't update correctly and the "Recently in Development" items don't load for 10 seconds. - setTimeout(() => { - if (this.props.isAuthenticated) this._startPollingForProjects(); - }, 1); - - // NOTE(brentvatne): if we add QR code button to the menu again, we'll need to - // find a way to move this listener up to the root of the app in order to ensure - // that it has been registered regardless of whether we have been on the project - // screen in the home app - addListenerWithNativeCallback('ExponentKernel.showQRReader', async () => { - // @ts-ignore - this.props.navigation.navigate('QRCode'); - return { success: true }; - }); - } - - componentWillUnmount() { - this._stopPollingForProjects(); - this._changeEventListener?.remove(); - Connectivity.removeListener(this._updateConnectivity); - } - - render() { - const { projects, isRefreshing, data } = this.state; - - return ( - - - - - } - bounces - key={Platform.OS === 'ios' ? this.props.allHistory.count() : 'scroll-view'} - style={styles.container} - contentInsetAdjustmentBehavior="automatic" - contentContainerStyle={styles.contentContainer}> - - - - {projects?.length ? ( - - {projects.map((project, i) => ( - - - {projects.length > 1 && i !== projects.length - 1 ? ( - - ) : null} - - ))} - {FeatureFlags.ENABLE_PROJECT_TOOLS && FeatureFlags.ENABLE_QR_CODE_BUTTON ? ( - - ) : null} - {FeatureFlags.ENABLE_PROJECT_TOOLS && FeatureFlags.ENABLE_CLIPBOARD_BUTTON ? ( - - ) : null} - - ) : ( - - )} - {this.props.recentHistory.count() ? ( - <> - - - - - ) : null} - - {data?.apps.length && this.props.accountName ? ( - <> - - - 3} - /> - - ) : null} - - {data?.snacks.length && this.props.accountName ? ( - <> - - - 3} - /> - - ) : null} - - - - - ); - } - - componentDidUpdate(prevProps: Props) { - if (!prevProps.isFocused && this.props.isFocused) { - this._fetchProjectsAsync(); - } - - if (!prevProps.isAuthenticated && this.props.isAuthenticated) { - this._startPollingForProjects(); - } - - if (prevProps.isAuthenticated && !this.props.isAuthenticated) { - // Remove all projects except Snack, because they are tied to device id - // Fix this lint warning when converting to hooks - // eslint-disable-next-line - this.setState(({ projects }) => ({ - projects: projects.filter((p) => p.source === 'snack'), - data: undefined, - })); - - this._stopPollingForProjects(); - } - } - - private _updateConnectivity = (isAvailable: boolean): void => { - if (isAvailable !== this.state.isNetworkAvailable) { - this.setState({ isNetworkAvailable: isAvailable }); - } - }; - - private _maybeResumePollingFromAppState = (nextAppState: string): void => { - if (nextAppState === 'active' && !this._projectPolling) { - this._startPollingForProjects(); - } else { - this._stopPollingForProjects(); - } - }; - - private _handlePressClearHistory = () => { - this.props.dispatch(HistoryActions.clearHistory()); - }; - - private _startPollingForProjects = async () => { - await this._fetchProjectsAsync(); - this._projectPolling = setInterval(this._fetchProjectsAsync, PROJECT_UPDATE_INTERVAL); - }; - - private _stopPollingForProjects = async () => { - if (this._projectPolling) { - clearInterval(this._projectPolling); - } - this._projectPolling = undefined; - }; - - private _fetchProjectsAsync = async () => { - if (!this.props.isAuthenticated) return; - - const { accountName } = this.props; - - try { - const api = new APIV2Client(); - - const [projects, graphQLResponse] = await Promise.all([ - api.sendAuthenticatedApiV2Request('development-sessions', { - method: 'GET', - }), - accountName - ? ApolloClient.query({ - query: HomeScreenDataDocument, - variables: { - accountName, - platform: Platform.OS === 'ios' ? AppPlatform.Ios : AppPlatform.Android, - }, - fetchPolicy: 'network-only', - }) - : new Promise((resolve) => { - resolve(undefined); - }), - ]); - - this.setState({ projects, data: graphQLResponse?.data.account.byName }); - } catch (e) { - // this doesn't really matter, we will try again later - if (__DEV__) { - console.error(e); - } - } - }; - - private _handleRefreshAsync = async () => { - this.setState({ isRefreshing: true }); - - try { - await Promise.all([ - this._fetchProjectsAsync(), - new Promise((resolve) => setTimeout(resolve, 1000)), - ]); - } catch { - // not sure what to do here, maybe nothing? - } finally { - this.setState({ isRefreshing: false }); - } - }; - - private _handlePressHelpProjects = () => { - if (!this.state.isNetworkAvailable) { - Alert.alert( - 'No network connection available', - `You must be connected to the internet to view a list of your projects open in development.` - ); - } - - const baseMessage = `Make sure you are signed in to the same Expo account on your computer and this app. Also verify that your computer is connected to the internet, and ideally to the same Wi-Fi network as your mobile device. Lastly, ensure that you are using the latest version of Expo CLI. Pull to refresh to update.`; - const message = Platform.select({ - ios: isDevice - ? baseMessage - : `${baseMessage} If this still doesn't work, press the + icon on the header to type the project URL manually.`, - android: baseMessage, - }); - Alert.alert('Troubleshooting', message); - }; -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - contentContainer: { - padding: spacing[4], - }, - projectImageStyle: { - borderWidth: 1, - borderColor: 'rgba(0, 0, 32, 0.1)', - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/ProjectsSection.tsx b/apps/expo-go/src/screens/HomeScreen/ProjectsSection.tsx deleted file mode 100644 index 99da918de909ed..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/ProjectsSection.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { ChevronDownIcon } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { Divider, Row, useExpoTheme, View, Text } from 'expo-dev-client-components'; -import React, { Fragment } from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { ProjectsListItem } from '../../components/ProjectsListItem'; -import { CommonAppDataFragment } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -type Props = { - apps: CommonAppDataFragment[]; - showMore: boolean; - accountName: string; -}; - -export function ProjectsSection({ apps, showMore, accountName }: Props) { - const theme = useExpoTheme(); - const navigation = useNavigation>(); - - function onSeeAllProjectsPress() { - navigation.push('ProjectsList', { accountName }); - } - - return ( - - {apps.map((project, i) => { - if (!project) return null; - - return ( - - - {i < apps.length - 1 && } - - ); - })} - {showMore && ( - - - - - See all projects - - - - - - )} - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedHeader.tsx b/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedHeader.tsx deleted file mode 100644 index eaa28b9524a14d..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedHeader.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { Heading, Row, Text } from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -type Props = { - onClearPress: () => void; -}; - -export function RecentlyOpenedHeader({ onClearPress }: Props) { - return ( - - - Recently opened - - - - CLEAR - - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedListItem/index.tsx b/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedListItem/index.tsx deleted file mode 100644 index eb458664ab7464..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedListItem/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { ChevronDownIcon, spacing } from '@expo/styleguide-native'; -import { Text, useExpoTheme, View } from 'expo-dev-client-components'; -import * as React from 'react'; -import { View as RNView, StyleSheet, ViewStyle, Share, Image } from 'react-native'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import * as UrlUtils from '../../../utils/UrlUtils'; - -type Props = { - style?: ViewStyle; - disabled?: boolean; - title?: string; - url: string; - onPress?: () => void; - iconUrl?: string; -}; - -export function RecentlyOpenedListItem({ title, url, disabled, style, onPress, iconUrl }: Props) { - const theme = useExpoTheme(); - - const handleLongPress = () => { - const message = UrlUtils.normalizeUrl(url); - Share.share({ - title: url, - message, - url: message, - }); - }; - - return ( - - - - {iconUrl ? : null} - - {title} - - - - - - - - ); -} - -const styles = StyleSheet.create({ - infoText: { - marginTop: spacing[2], - }, - releaseChannel: { - marginTop: spacing[2], - }, - container: { - flexDirection: 'row', - padding: spacing[4], - alignItems: 'center', - }, - disabled: { - opacity: 0.5, - }, - contentContainer: { - backgroundColor: 'transparent', - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - textContainer: { - flex: 1, - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'flex-start', - }, - chevronRightContainer: { - alignSelf: 'center', - marginStart: spacing[2], - }, - icon: { - width: 40, - height: 40, - marginRight: 8, - borderRadius: 8, - alignSelf: 'center', - backgroundColor: 'transparent', - }, - row: { - flexDirection: 'row', - alignItems: 'center', - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedSection.tsx b/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedSection.tsx deleted file mode 100644 index 70a41954a7f745..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/RecentlyOpenedSection.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Divider, View } from 'expo-dev-client-components'; -import React, { Fragment } from 'react'; -import { Linking } from 'react-native'; - -import { RecentlyOpenedListItem } from './RecentlyOpenedListItem'; -import { HistoryList } from '../../types'; - -type Props = { - recentHistory: HistoryList; -}; - -export function RecentlyOpenedSection({ recentHistory }: Props) { - return ( - - {recentHistory.map((project, i) => { - if (!project) return null; - - // EAS Update app names are under the extra.expoClient.name key - const title = - (project.manifest && 'extra' in project.manifest - ? project.manifest.extra?.expoClient?.name - : undefined) ?? - (project.manifest && 'name' in project.manifest - ? String(project.manifest.name) - : undefined); - - const iconUrl = - project.manifest && 'extra' in project.manifest - ? // @ts-expect-error iconUrl exists only for local development - project.manifest?.extra?.expoClient?.iconUrl - : undefined; - - return ( - - { - Linking.openURL(project.url); - }} - /> - {i < recentHistory.count() - 1 && } - - ); - })} - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/SnacksSection.tsx b/apps/expo-go/src/screens/HomeScreen/SnacksSection.tsx deleted file mode 100644 index 37c29ee5170ceb..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/SnacksSection.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { ChevronDownIcon } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { Divider, Row, useExpoTheme, View, Text } from 'expo-dev-client-components'; -import React, { Fragment } from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -import { SnacksListItem } from '../../components/SnacksListItem'; -import { CommonSnackDataFragment } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -type Props = { - snacks: CommonSnackDataFragment[]; - showMore: boolean; - accountName: string; -}; - -export function SnacksSection({ snacks, showMore, accountName }: Props) { - const theme = useExpoTheme(); - - const navigation = useNavigation>(); - - function onSeeAllSnacksPress() { - navigation.push('SnacksList', { accountName }); - } - - return ( - - {snacks.map((snack, i) => { - if (!snack) return null; - - return ( - - - {i < snacks.length - 1 && } - - ); - })} - {showMore && ( - - - - - See all snacks - - - - - - )} - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/AndroidMessage.tsx b/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/AndroidMessage.tsx deleted file mode 100644 index 4e4391daf3f48c..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/AndroidMessage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Text } from 'expo-dev-client-components'; -import { Linking } from 'react-native'; - -export function AndroidMessage() { - return ( - <> - - If you have automatic updates enabled for this app, we recommend{' '} - - disabling - {' '} - it to avoid disruption. - - - If you ever need to open a project from an earlier SDK version, install the{' '} - Linking.openURL('https://expo.dev/go')}> - compatible version - {' '} - of Expo Go. - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/IosMessage.tsx b/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/IosMessage.tsx deleted file mode 100644 index a078eb26642325..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/IosMessage.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Text } from 'expo-dev-client-components'; -import { Linking } from 'react-native'; - -export function IosMessage() { - return ( - <> - - In order to ensure that you can upgrade at your own pace, we recommend{' '} - - Linking.openURL('https://docs.expo.dev/develop/development-builds/expo-go-to-dev-build') - }> - migrating to a development build - - . - - - To continue using this version of Expo Go, you can{' '} - - disable automatic app updates - {' '} - from the App Store settings before the new version is released. - - - ); -} diff --git a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/index.tsx b/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/index.tsx deleted file mode 100644 index 54e4add8853c6b..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { iconSize, XIcon, WarningIcon, spacing } from '@expo/styleguide-native'; -import { Row, Spacer, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import { useEffect, useState } from 'react'; -import { Pressable, StyleSheet, Platform } from 'react-native'; - -import { AndroidMessage } from './AndroidMessage'; -import { IosMessage } from './IosMessage'; -import { shouldShowUpgradeWarningAsync } from './utils'; - -type Props = { - collapsible?: boolean; -}; - -export function UpgradeWarning({ collapsible = false }: Props) { - const [shouldShow, setShouldShow] = useState(false); - const [betaSdkVersion, setBetaSdkVersion] = useState(undefined); - const [isCollapsed, setIsCollapsed] = useState(collapsible); - const theme = useExpoTheme(); - const dismissUpgradeWarning = () => { - setShouldShow(false); - }; - - useEffect(() => { - shouldShowUpgradeWarningAsync().then(({ shouldShow, betaSdkVersion }) => { - setShouldShow(shouldShow); - setBetaSdkVersion(betaSdkVersion); - }); - }, []); - - if (!shouldShow) { - return null; - } - - return ( - <> - - - - - - - - - New Expo Go version coming soon! - - - - A new version of Expo Go will be released to the store soon, and it will{' '} - - only support SDK {betaSdkVersion} - - . - - {!isCollapsed && (Platform.OS === 'ios' ? : )} - - {collapsible && ( - setIsCollapsed(!isCollapsed)}> - - {isCollapsed ? 'Show more' : 'Show less'} - - - )} - - - - - ); -} - -const styles = StyleSheet.create({ - dismissButton: { - position: 'absolute', - top: spacing[4], - right: spacing[4], - zIndex: 1, - }, - content: { - gap: spacing[1], - }, -}); diff --git a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/utils.ts b/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/utils.ts deleted file mode 100644 index 2100494c985fd7..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/UpgradeWarning/utils.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as Device from 'expo-device'; -import Environment from 'src/utils/Environment'; - -const SDK_VERSION_REGEXP = new RegExp(/\b(\d*)\.0\.0/); - -type SdkVersionFromApiType = { - androidClientUrl?: string; - androidClientVersion?: string; - expoVersion?: string; - facebookReactNativeVersion?: string; - facebookReactVersion?: string; - iosClientUrl?: string; - iosClientVersion?: string; - releaseNoteUrl?: string; -}; - -type SdkVersionTypeWithSdkType = SdkVersionFromApiType & { - sdk: string; - isLatest?: boolean; - isBeta?: boolean; -}; - -type VersionsApiResponseType = { - sdkVersions: Record; -}; - -/** - * Show the message if: - * - On a real device AND - * - The latest SDK is in beta AND - * - The app is running the latest published SDK (this avoids showing on Android when the user has installed an older version) - */ -export async function shouldShowUpgradeWarningAsync(): Promise<{ - shouldShow: boolean; - betaSdkVersion?: string; -}> { - if (!Device.isDevice) { - return { - shouldShow: false, - }; - } - - const result = await fetch('https://api.expo.dev/v2/versions'); - - try { - const data: VersionsApiResponseType = await result.json(); - - const publishedVersions = Object.keys(data.sdkVersions) - .map((sdk) => ({ - ...data.sdkVersions[sdk], - sdk: sdk.match(SDK_VERSION_REGEXP)?.[1], - })) - .filter((version) => !!version.sdk) as SdkVersionTypeWithSdkType[]; - - const lastVersion = publishedVersions[publishedVersions.length - 1]; - const penultimateVersion = publishedVersions[publishedVersions.length - 2]; - const currentIsLatestPublished = Environment.supportedSdksString === penultimateVersion.sdk; - const latestIsBeta = !lastVersion.releaseNoteUrl; - - return { - shouldShow: Boolean(currentIsLatestPublished && latestIsBeta), - betaSdkVersion: lastVersion.sdk, - }; - } catch {} - - return { - shouldShow: false, - }; -} diff --git a/apps/expo-go/src/screens/HomeScreen/index.tsx b/apps/expo-go/src/screens/HomeScreen/index.tsx deleted file mode 100644 index a0e73ac28e1cb4..00000000000000 --- a/apps/expo-go/src/screens/HomeScreen/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import { View, useCurrentTheme, useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { HomeScreenView } from './HomeScreenView'; -import { HomeScreenDataQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; -import { useDispatch, useSelector } from '../../redux/Hooks'; -import { HistoryList } from '../../types'; -import { useAccountName } from '../../utils/AccountNameContext'; -import { useInitialData } from '../../utils/InitialDataContext'; -import hasSessionSecret from '../../utils/hasSessionSecret'; - -type NavigationProps = StackScreenProps & { - homeScreenData?: Exclude; -}; - -export function HomeScreen(props: NavigationProps) { - const [isFocused, setFocused] = React.useState(true); - React.useEffect(() => { - const unsubscribe = props.navigation.addListener('focus', () => { - setFocused(true); - }); - const unsubscribeBlur = props.navigation.addListener('blur', () => { - setFocused(false); - }); - - return () => { - unsubscribe(); - unsubscribeBlur(); - }; - }, [props.navigation]); - - const dispatch = useDispatch(); - const { recentHistory, allHistory, isAuthenticated } = useSelector( - React.useCallback((data) => { - const { history } = data.history; - - return { - recentHistory: history.take(10) as HistoryList, - allHistory: history as HistoryList, - isAuthenticated: hasSessionSecret(data.session), - }; - }, []) - ); - - const theme = useExpoTheme(); - const themeType = useCurrentTheme(); - const { accountName } = useAccountName(); - const { homeScreenData } = useInitialData(); - const insets = useSafeAreaInsets(); - - return ( - - - - - ); -} diff --git a/apps/expo-go/src/screens/ProjectScreen/EASUpdateLaunchSection.tsx b/apps/expo-go/src/screens/ProjectScreen/EASUpdateLaunchSection.tsx deleted file mode 100644 index a228355d4b3c86..00000000000000 --- a/apps/expo-go/src/screens/ProjectScreen/EASUpdateLaunchSection.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { ChevronDownIcon } from '@expo/styleguide-native'; -import { useNavigation } from '@react-navigation/native'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { Divider, Row, View, Text, useExpoTheme } from 'expo-dev-client-components'; -import React, { Fragment } from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import { BranchManifest } from 'src/screens/BranchListScreen/BranchListView'; - -import { BranchListItem } from '../../components/BranchListItem'; -import { ProjectsQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -type ProjectPageApp = ProjectsQuery['app']['byId']; - -export function EASUpdateLaunchSection({ app }: { app: ProjectPageApp }) { - const branchesToRender = app.updateBranches.filter( - (updateBranch) => updateBranch.updates.length > 0 - ); - - const branchManifests: BranchManifest[] = branchesToRender.slice(0, 3).map((branch) => ({ - name: branch.name, - id: branch.id, - latestUpdate: branch.updates[0], - })); - - const theme = useExpoTheme(); - const navigation = useNavigation>(); - - function onSeeAllBranchesPress() { - navigation.navigate('Branches', { appId: app.id }); - } - - if (branchManifests.length === 0) { - return ( - - No EAS Update branches - - ); - } - - return ( - - {branchManifests.map((branch, i) => { - return ( - - - {i < branchManifests.length - 1 && } - - ); - })} - {branchesToRender.length > 3 && ( - - - - - See all branches - - - - - - )} - - ); -} diff --git a/apps/expo-go/src/screens/ProjectScreen/ProjectContainer.tsx b/apps/expo-go/src/screens/ProjectScreen/ProjectContainer.tsx deleted file mode 100644 index aae598cb3fc697..00000000000000 --- a/apps/expo-go/src/screens/ProjectScreen/ProjectContainer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import * as React from 'react'; -import { Platform } from 'react-native'; - -import { ProjectView } from './ProjectView'; -import { AppPlatform, useProjectsQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -export function ProjectContainer( - props: { appId: string } & StackScreenProps -) { - const query = useProjectsQuery({ - fetchPolicy: 'cache-and-network', - variables: { - appId: props.appId, - platform: Platform.OS === 'ios' ? AppPlatform.Ios : AppPlatform.Android, - }, - }); - return ; -} diff --git a/apps/expo-go/src/screens/ProjectScreen/ProjectHeader.tsx b/apps/expo-go/src/screens/ProjectScreen/ProjectHeader.tsx deleted file mode 100644 index c3724b2ca643d0..00000000000000 --- a/apps/expo-go/src/screens/ProjectScreen/ProjectHeader.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Row, useExpoTheme, View, Text, padding } from 'expo-dev-client-components'; -import * as React from 'react'; - -import { CappedWidthContainerView } from '../../components/Views'; -import { ProjectsQuery } from '../../graphql/types'; - -type ProjectPageApp = ProjectsQuery['app']['byId']; - -export function ProjectHeader(props: { app: ProjectPageApp }) { - const theme = useExpoTheme(); - console.log(padding.padding.medium); - return ( - - - - - {props.app.name} - - - {props.app.slug} - - - Owned by {props.app.ownerAccount.name} - - - - - ); -} diff --git a/apps/expo-go/src/screens/ProjectScreen/ProjectView.tsx b/apps/expo-go/src/screens/ProjectScreen/ProjectView.tsx deleted file mode 100644 index 82259ff68ada87..00000000000000 --- a/apps/expo-go/src/screens/ProjectScreen/ProjectView.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { StackScreenProps } from '@react-navigation/stack'; -import dedent from 'dedent'; -import { padding, Spacer, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import * as React from 'react'; -import { ActivityIndicator } from 'react-native'; -import { SectionHeader } from 'src/components/SectionHeader'; - -import { EASUpdateLaunchSection } from './EASUpdateLaunchSection'; -import { ProjectHeader } from './ProjectHeader'; -import ScrollView from '../../components/NavigationScrollView'; -import ShareProjectButton from '../../components/ShareProjectButton'; -import { CappedWidthContainerView } from '../../components/Views'; -import { ProjectsQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -const ERROR_TEXT = dedent` - An unexpected error has occurred. - Sorry about this. We will resolve the issue as soon as possible. -`; - -type Props = { - loading: boolean; - error?: Error; - data?: ProjectsQuery; -} & StackScreenProps; - -export function ProjectView({ loading, error, data, navigation }: Props) { - const theme = useExpoTheme(); - - let contents; - if (error && !data?.app?.byId) { - console.log(error); - contents = ( - - {ERROR_TEXT} - - ); - } else if (loading || !data?.app?.byId) { - contents = ( - - - - ); - } else { - const app = data.app.byId; - - contents = ( - - - - - - - - - ); - } - - React.useEffect(() => { - if (data?.app?.byId) { - const fullName = data?.app.byId.fullName; - const title = data?.app.byId.name ?? fullName; - navigation.setOptions({ - title, - headerRight: () => , - }); - } - }, [navigation, data?.app?.byId]); - - return {contents}; -} diff --git a/apps/expo-go/src/screens/ProjectScreen/WarningBox.tsx b/apps/expo-go/src/screens/ProjectScreen/WarningBox.tsx deleted file mode 100644 index c71655cfa10b42..00000000000000 --- a/apps/expo-go/src/screens/ProjectScreen/WarningBox.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import { useExpoTheme, Text, Spacer, View } from 'expo-dev-client-components'; -import * as React from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -export function WarningBox({ - message, - showLearnMore, - onLearnMorePress, -}: { - message: string; - showLearnMore?: boolean; - onLearnMorePress?: () => void; -}) { - const theme = useExpoTheme(); - - const learnMoreButton = showLearnMore ? ( - <> - - - - Learn more - - - - ) : null; - return ( - - - {message} - - {learnMoreButton} - - ); -} diff --git a/apps/expo-go/src/screens/ProjectScreen/index.tsx b/apps/expo-go/src/screens/ProjectScreen/index.tsx deleted file mode 100644 index 2cb5d80bcfdf3e..00000000000000 --- a/apps/expo-go/src/screens/ProjectScreen/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import * as React from 'react'; - -import { ProjectContainer } from './ProjectContainer'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -export function ProjectScreen(props: StackScreenProps) { - const { id } = props.route.params; - - return ; -} diff --git a/apps/expo-go/src/screens/ProjectsListScreen/ProjectList.tsx b/apps/expo-go/src/screens/ProjectsListScreen/ProjectList.tsx deleted file mode 100644 index 14b8073a112f7d..00000000000000 --- a/apps/expo-go/src/screens/ProjectsListScreen/ProjectList.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { spacing } from '@expo/styleguide-native'; -import dedent from 'dedent'; -import { Divider, useExpoTheme } from 'expo-dev-client-components'; -import * as React from 'react'; -import { FlatList, ActivityIndicator, ListRenderItem, View as RNView } from 'react-native'; - -import PrimaryButton from '../../components/PrimaryButton'; -import { ProjectsListItem } from '../../components/ProjectsListItem'; -import { StyledText } from '../../components/Text'; -import { CappedWidthContainerView } from '../../components/Views'; -import SharedStyles from '../../constants/SharedStyles'; -import { CommonAppDataFragment } from '../../graphql/types'; - -const NETWORK_ERROR_TEXT = dedent` - Your connection appears to be offline. - Check back when you have a better connection. -`; - -const SERVER_ERROR_TEXT = dedent` - An unexpected server error has occurred. - Sorry about this. We will resolve the issue as soon as quickly as possible. -`; - -type Props = { - data: { apps?: CommonAppDataFragment[]; appCount?: number }; - loadMoreAsync: () => Promise; - loading: boolean; - error?: Error; - refetch: () => Promise; -}; - -export function ProjectList(props: Props) { - const [isReady, setReady] = React.useState(false); - const isRetrying = React.useRef(false); - - const theme = useExpoTheme(); - - React.useEffect(() => { - const readyTimer = setTimeout(() => { - setReady(true); - }, 500); - return () => { - clearTimeout(readyTimer); - }; - }, []); - - if (!isReady) { - return ( - - - - ); - } - - if (!props.data?.apps?.length) { - if (!props.loading && props.error) { - // Error - // NOTE(brentvatne): sorry for this - const isConnectionError = props.error.message.includes('No connection available'); - - const refetchDataAsync = async () => { - if (isRetrying.current) return; - isRetrying.current = true; - try { - await props.refetch(); - } catch (e) { - console.log({ e }); - // Error! - } finally { - isRetrying.current = false; - } - }; - - return ( - - - {isConnectionError ? NETWORK_ERROR_TEXT : SERVER_ERROR_TEXT} - - - - Try again - - - ); - } - - return ; - } - - return ; -} - -function ProjectListView({ data, loadMoreAsync }: Props) { - const isLoading = React.useRef(false); - const theme = useExpoTheme(); - const extractKey = (item: CommonAppDataFragment) => item.id; - - const currentAppCount = data.apps?.length ?? 0; - const totalAppCount = data.appCount ?? 0; - const canLoadMore = currentAppCount < totalAppCount; - - const handleLoadMoreAsync = async () => { - if (isLoading.current || !canLoadMore) return; - isLoading.current = true; - - try { - await loadMoreAsync(); - } catch (e) { - console.error(e); - } finally { - isLoading.current = false; - } - }; - - const renderItem: ListRenderItem = React.useCallback( - ({ item: app, index }) => { - return ( - - ); - }, - [data.apps] - ); - - return ( - - } - onEndReachedThreshold={0.2} - onEndReached={handleLoadMoreAsync} - /> - - ); -} diff --git a/apps/expo-go/src/screens/ProjectsListScreen/index.tsx b/apps/expo-go/src/screens/ProjectsListScreen/index.tsx deleted file mode 100644 index 742063a5dc2f61..00000000000000 --- a/apps/expo-go/src/screens/ProjectsListScreen/index.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import * as React from 'react'; -import { Platform } from 'react-native'; - -import { ProjectList } from './ProjectList'; -import { AppPlatform, useHome_AccountAppsQuery } from '../../graphql/types'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; - -function useProjectsForAccountQuery({ accountName }: { accountName: string }) { - const { data, fetchMore, loading, error, refetch } = useHome_AccountAppsQuery({ - variables: { - accountName, - limit: 15, - offset: 0, - platform: Platform.OS === 'ios' ? AppPlatform.Ios : AppPlatform.Android, - }, - fetchPolicy: 'cache-and-network', - }); - - const apps = data?.account.byName.apps; - const appCount = data?.account.byName.appCount; - - const loadMoreAsync = React.useCallback(() => { - return fetchMore({ - variables: { - offset: apps?.length || 0, - }, - }); - }, [fetchMore, apps]); - - return { - loading, - error, - refetch, - data: { - ...data, - appCount, - apps, - }, - loadMoreAsync, - }; -} - -export function ProjectsListScreen({ route }: StackScreenProps) { - const accountName = route.params.accountName; - - const query = useProjectsForAccountQuery({ accountName }); - - return ; -} diff --git a/apps/expo-go/src/screens/QRCodeScreen.tsx b/apps/expo-go/src/screens/QRCodeScreen.tsx deleted file mode 100644 index df0a200c303828..00000000000000 --- a/apps/expo-go/src/screens/QRCodeScreen.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { StackScreenProps } from '@react-navigation/stack'; -import { BlurView } from 'expo-blur'; -import React from 'react'; -import { Linking, Platform, StatusBar, StyleSheet, Text, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import CameraView from '../components/Camera'; -import QRFooterButton from '../components/QRFooterButton'; -import QRIndicator from '../components/QRIndicator'; -import { ModalStackRoutes } from '../navigation/Navigation.types'; - -type State = { - isVisible: boolean; - url: null | string; -}; - -const initialState: State = { isVisible: Platform.OS === 'ios', url: null }; - -export default function BarCodeScreen(props: StackScreenProps) { - const [state, setState] = React.useReducer( - (props: State, state: Partial): State => ({ ...props, ...state }), - initialState - ); - const [isLit, setLit] = React.useState(false); - - React.useEffect(() => { - let timeout: ReturnType; - if (!state.isVisible) { - timeout = setTimeout(() => { - setState({ isVisible: true }); - }, 800); - } - return () => { - clearTimeout(timeout); - }; - }, []); - - React.useEffect(() => { - if (!state.isVisible && state.url) { - openUrl(state.url); - } - }, [state.isVisible, state.url]); - - const _handleBarCodeScanned = throttle(({ data: url }) => { - setState({ isVisible: false, url }); - }, 1000); - - const openUrl = (url: string) => { - props.navigation.pop(); - - setTimeout( - () => { - // note(brentvatne): Manually reset the status bar before opening the - // experience so that we restore the correct status bar color when - // returning to home - StatusBar.setBarStyle('default'); - Linking.openURL(url); - }, - Platform.select({ - ios: 16, - // note(brentvatne): Give the modal a bit of time to dismiss on Android - default: 500, - }) - ); - }; - - const onCancel = React.useCallback(() => { - if (Platform.OS === 'ios') { - props.navigation.pop(); - } else { - props.navigation.goBack(); - } - }, []); - - const onFlashToggle = React.useCallback(() => { - setLit((isLit) => !isLit); - }, []); - - const { top, bottom } = useSafeAreaInsets(); - - return ( - - {state.isVisible ? ( - - ) : null} - - - Scan an Expo QR code - - - - - - - - - - - - ); -} - -function Hint({ children }: { children: string }) { - return ( - - {children} - - ); -} - -function throttle void>(func: T, delay: number): T { - let lastCall = 0; - - return function (...args: Parameters) { - const now = Date.now(); - if (now - lastCall >= delay) { - lastCall = now; - func(...args); - } - } as T; -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - justifyContent: 'center', - alignItems: 'center', - }, - hint: { - paddingHorizontal: 16, - paddingVertical: 20, - borderRadius: 16, - overflow: 'hidden', - justifyContent: 'center', - alignItems: 'center', - }, - header: { - position: 'absolute', - left: 0, - right: 0, - alignItems: 'center', - }, - headerText: { - color: '#fff', - backgroundColor: 'transparent', - textAlign: 'center', - fontSize: 16, - fontWeight: '500', - }, - footer: { - position: 'absolute', - left: 0, - right: 0, - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: '10%', - }, -}); diff --git a/apps/expo-go/src/screens/SettingsScreen/CheckListItem.tsx b/apps/expo-go/src/screens/SettingsScreen/CheckListItem.tsx deleted file mode 100644 index 09dae185dbe921..00000000000000 --- a/apps/expo-go/src/screens/SettingsScreen/CheckListItem.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { CheckIcon, iconSize } from '@expo/styleguide-native'; -import { Row, Spacer, Text, useExpoTheme } from 'expo-dev-client-components'; -import React, { ReactNode } from 'react'; -import { TouchableOpacity } from 'react-native-gesture-handler'; - -type Props = { - onPress: () => void; - icon?: ReactNode; - title: string; - checked?: boolean; -}; - -export function CheckListItem({ onPress, icon, title, checked }: Props) { - const theme = useExpoTheme(); - - return ( - - - - {icon} - {icon ? : null} - - {title} - - - {checked ? : null} - - - ); -} diff --git a/apps/expo-go/src/screens/SettingsScreen/ConstantsSection.tsx b/apps/expo-go/src/screens/SettingsScreen/ConstantsSection.tsx deleted file mode 100644 index 99fbe422c794b1..00000000000000 --- a/apps/expo-go/src/screens/SettingsScreen/ConstantsSection.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import Constants from 'expo-constants'; -import { View, Divider } from 'expo-dev-client-components'; -import * as React from 'react'; -import { Clipboard, Alert } from 'react-native'; - -import { ConstantItem } from '../../components/ConstantItem'; -import { SectionHeader } from '../../components/SectionHeader'; -import Environment from '../../utils/Environment'; - -export function ConstantsSection() { - const copyClientVersionToClipboard = () => { - if (Constants.expoVersion) { - Clipboard.setString(Constants.expoVersion); - Alert.alert('Clipboard', `The app's version has been copied to your clipboard.`); - } else { - // this should not ever happen - Alert.alert('Clipboard', `Something went wrong - the app's version is not available.`); - } - }; - - return ( - - - - {Constants.expoVersion ? ( - <> - - - - ) : null} - - - - ); -} diff --git a/apps/expo-go/src/screens/SettingsScreen/DeleteAccountSection.tsx b/apps/expo-go/src/screens/SettingsScreen/DeleteAccountSection.tsx deleted file mode 100644 index 24a2c66dbfb090..00000000000000 --- a/apps/expo-go/src/screens/SettingsScreen/DeleteAccountSection.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { TrashIcon } from '@expo/styleguide-native'; -import { useNavigation, NavigationProp } from '@react-navigation/native'; -import { Row, Spacer, Text, useExpoTheme, View } from 'expo-dev-client-components'; -import * as WebBrowser from 'expo-web-browser'; -import React from 'react'; - -import Config from '../../api/Config'; -import { Button } from '../../components/Button'; -import { SectionHeader } from '../../components/SectionHeader'; -import { HomeStackRoutes } from '../../navigation/Navigation.types'; -import { useDispatch } from '../../redux/Hooks'; -import SessionActions from '../../redux/SessionActions'; -import { useAccountName } from '../../utils/AccountNameContext'; - -export function DeleteAccountSection() { - const theme = useExpoTheme(); - const { setAccountName } = useAccountName(); - const dispatch = useDispatch(); - const [isDeleting, setIsDeleting] = React.useState(false); - const navigation = useNavigation>(); - const [deletionError, setDeletionError] = React.useState(null); - const mounted = React.useRef(true); - - const _handleDeleteAccount = async () => { - if (isDeleting) { - return; - } - setDeletionError(null); - setIsDeleting(true); - - try { - const redirectBase = 'expauth://after-delete'; - const authSessionURL = `${ - Config.website.origin - }/settings/delete-user-expo-go?post_delete_redirect_uri=${encodeURIComponent(redirectBase)}`; - const result = await WebBrowser.openAuthSessionAsync(authSessionURL, redirectBase, { - /** note(brentvatne): We should disable the showInRecents option when - * https://github.com/expo/expo/issues/8072 is resolved. This workaround - * prevents the Chrome Custom Tabs activity from closing when the user - * switches from the login / sign up form to a password manager or 2fa - * app. The downside of using this flag is that the browser window will - * remain open in the background after authentication completes. */ - showInRecents: true, - }); - - if (!mounted.current) { - return; - } - - if (result.type === 'success') { - setAccountName(undefined); - dispatch(SessionActions.signOut()); - navigation.navigate('Home'); - } - } catch (e: any) { - // TODO(wschurman): Put this into Sentry - console.error({ e }); - setDeletionError(e.message); - } finally { - setIsDeleting(false); - } - }; - - return ( - - - - - - - - - Delete your account - - - - - This action is irreversible. It will delete your personal account, projects, and - activity. - - - {deletionError ? ( - <> - - {deletionError} - - - - ) : null} - - diff --git a/apps/native-component-list/src/screens/UI/ColorPickerScreen.ios.tsx b/apps/native-component-list/src/screens/UI/ColorPickerScreen.ios.tsx index 79889db2d3b4eb..71f1f6487c9be6 100644 --- a/apps/native-component-list/src/screens/UI/ColorPickerScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/ColorPickerScreen.ios.tsx @@ -1,15 +1,12 @@ -import { ColorPicker, Host, Switch, VStack } from '@expo/ui/swift-ui'; +import { ColorPicker, Form, Host, Section, Switch, Text, VStack } from '@expo/ui/swift-ui'; import * as React from 'react'; -import { ScrollView, Text } from 'react-native'; - -import { Page, Section } from '../../components/Page'; export default function ColorPickerScreen() { const [color, setColor] = React.useState('blue'); const [supportsOpacity, setSupportsOpacity] = React.useState(false); return ( - - + +
Color: {color} @@ -23,13 +20,13 @@ export default function ColorPickerScreen() { label="Select a color" selection={color} supportsOpacity={supportsOpacity} - onValueChanged={setColor} + onSelectionChange={setColor} />
- - +
+
); } diff --git a/apps/native-component-list/src/screens/UI/ContextMenuScreen.android.tsx b/apps/native-component-list/src/screens/UI/ContextMenuScreen.android.tsx index 47c53f0c6a8c85..3962617641c251 100644 --- a/apps/native-component-list/src/screens/UI/ContextMenuScreen.android.tsx +++ b/apps/native-component-list/src/screens/UI/ContextMenuScreen.android.tsx @@ -1,4 +1,4 @@ -import { Button, Switch, ContextMenu, Submenu } from '@expo/ui/jetpack-compose'; +import { Button, Switch, ContextMenu, Submenu, Host } from '@expo/ui/jetpack-compose'; // import { useVideoPlayer, VideoView } from 'expo-video'; import * as React from 'react'; import { View, /* StyleSheet, */ Text } from 'react-native'; @@ -28,91 +28,95 @@ export default function ContextMenuScreen() { Theme - + + + + + + + + + + + + + + +
+ + - + + - - + /> - -
-
- - - - - - - - - - - +
diff --git a/apps/native-component-list/src/screens/UI/ContextMenuScreen.ios.tsx b/apps/native-component-list/src/screens/UI/ContextMenuScreen.ios.tsx index 79699b42ff3d97..db3e4b646d53a5 100644 --- a/apps/native-component-list/src/screens/UI/ContextMenuScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/ContextMenuScreen.ios.tsx @@ -12,12 +12,7 @@ import { Divider, RNHostView, } from '@expo/ui/swift-ui'; -import { - buttonStyle, - menuActionDismissBehavior, - pickerStyle, - tag, -} from '@expo/ui/swift-ui/modifiers'; +import { buttonStyle, foregroundStyle, pickerStyle, tag } from '@expo/ui/swift-ui/modifiers'; import { useVideoPlayer, VideoView } from 'expo-video'; import * as React from 'react'; import { View, StyleSheet, Text as RNText } from 'react-native'; @@ -39,8 +34,8 @@ export default function ContextMenuScreen() { return ( -
- +
+
-
+
-
- +
+
-
- - -
@@ -176,7 +151,7 @@ export default function ContextMenuScreen() { - Show menu + Show menu
diff --git a/apps/native-component-list/src/screens/UI/DatePickerScreen.ios.tsx b/apps/native-component-list/src/screens/UI/DatePickerScreen.ios.tsx index c3812b67fa4321..ba939f2acc9034 100644 --- a/apps/native-component-list/src/screens/UI/DatePickerScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/DatePickerScreen.ios.tsx @@ -17,6 +17,7 @@ import { tag, tint, Animation, + foregroundStyle, } from '@expo/ui/swift-ui/modifiers'; import { useState } from 'react'; @@ -102,7 +103,7 @@ export default function DatePickerScreen() { ))} - +
setSelectedDate(date)} modifiers={[datePickerStyle(styleOptions[styleIndex])]}> - Select date + + Select date + {selectedDate.toDateString()}
diff --git a/apps/native-component-list/src/screens/UI/DateTimePickerScreen.android.tsx b/apps/native-component-list/src/screens/UI/DateTimePickerScreen.android.tsx index baa25d7351938a..c47827a4a4a1fb 100644 --- a/apps/native-component-list/src/screens/UI/DateTimePickerScreen.android.tsx +++ b/apps/native-component-list/src/screens/UI/DateTimePickerScreen.android.tsx @@ -1,5 +1,10 @@ -import { DateTimePicker, DateTimePickerProps, Picker, Column } from '@expo/ui/jetpack-compose'; -import { Host } from '@expo/ui/swift-ui'; +import { + DateTimePicker, + DateTimePickerProps, + Picker, + Column, + Host, +} from '@expo/ui/jetpack-compose'; import * as React from 'react'; import { ScrollView, Text } from 'react-native'; diff --git a/apps/native-component-list/src/screens/UI/FormScreen.ios.tsx b/apps/native-component-list/src/screens/UI/FormScreen.ios.tsx index 7dacbdbaa8ccb4..313c050c2a839c 100644 --- a/apps/native-component-list/src/screens/UI/FormScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/FormScreen.ios.tsx @@ -72,7 +72,7 @@ export default function FormScreen() { label="Select a color" selection={color} supportsOpacity - onValueChanged={setColor} + onSelectionChange={setColor} /> Name: John Doe diff --git a/apps/native-component-list/src/screens/UI/GaugeScreen.ios.tsx b/apps/native-component-list/src/screens/UI/GaugeScreen.ios.tsx index a7cf0b053dfaed..e8404fe680526e 100644 --- a/apps/native-component-list/src/screens/UI/GaugeScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/GaugeScreen.ios.tsx @@ -1,63 +1,59 @@ -import { Gauge, Host, VStack } from '@expo/ui/swift-ui'; -import * as React from 'react'; -import { PlatformColor } from 'react-native'; - -import { Page, Section } from '../../components/Page'; - -const COLORS = [ - PlatformColor('systemGreen'), - PlatformColor('systemYellow'), - PlatformColor('systemRed'), -]; +import { Gauge, Host, List, Section, Text, Button } from '@expo/ui/swift-ui'; +import { gaugeStyle, tint } from '@expo/ui/swift-ui/modifiers'; +import { useState } from 'react'; export default function GaugeScreen() { + const [value, setValue] = useState(0.5); + return ( - -
- - - - - - -
-
- - - - - - -
-
- - - - - - -
-
+ + +
+ +
+
+ 50%} + minimumValueLabel={0} + maximumValueLabel={100}> + Usage + +
+
+ + Circular + + + Capacity + +
+
+ + Linear + + + Capacity + +
+
+ + +
+
+
); } diff --git a/apps/native-component-list/src/screens/UI/GlassEffectScreen.ios.tsx b/apps/native-component-list/src/screens/UI/GlassEffectScreen.ios.tsx index 712cf6c42c2fbc..4aadc55a1f2845 100644 --- a/apps/native-component-list/src/screens/UI/GlassEffectScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/GlassEffectScreen.ios.tsx @@ -17,6 +17,7 @@ import { background, cornerRadius, frame, + foregroundStyle, } from '@expo/ui/swift-ui/modifiers'; import { useId, useState } from 'react'; import { View } from 'react-native'; @@ -158,7 +159,9 @@ export default function GlassEffect() { }, }), ]}> - {isGlassExpanded ? 'Hide Tools' : 'Show More Tools'} + + {isGlassExpanded ? 'Hide Tools' : 'Show More Tools'} + diff --git a/apps/native-component-list/src/screens/UI/GridScreen.ios.tsx b/apps/native-component-list/src/screens/UI/GridScreen.ios.tsx index 327392efadcfec..98cca3321f3cca 100644 --- a/apps/native-component-list/src/screens/UI/GridScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/GridScreen.ios.tsx @@ -16,6 +16,7 @@ import { import { background, clipShape, + font, foregroundStyle, frame, gridCellAnchor, @@ -193,9 +194,7 @@ export default function GridScreen() { {/* spacing and alignment */} - - Grid settings - + Grid settings @@ -245,7 +244,7 @@ export default function GridScreen() { {/* Example small Grid */} - Anchor + Anchor @@ -330,7 +329,9 @@ export default function GridScreen() { {/* Example 1 */} setDisclosureGroupExpanded((prev) => ({ ...prev, example1: v }))} + onIsExpandedChange={(v) => + setDisclosureGroupExpanded((prev) => ({ ...prev, example1: v })) + } isExpanded={disclosureGroupExpanded.example1} label="Example #1"> @@ -366,7 +367,9 @@ export default function GridScreen() { {/* Example 2 */} setDisclosureGroupExpanded((prev) => ({ ...prev, example2: v }))} + onIsExpandedChange={(v) => + setDisclosureGroupExpanded((prev) => ({ ...prev, example2: v })) + } isExpanded={disclosureGroupExpanded.example2} label="Example #2"> diff --git a/apps/native-component-list/src/screens/UI/ListScreen.ios.tsx b/apps/native-component-list/src/screens/UI/ListScreen.ios.tsx index 8a66166569d01b..d02b06db3e583b 100644 --- a/apps/native-component-list/src/screens/UI/ListScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/ListScreen.ios.tsx @@ -9,13 +9,12 @@ import { type ListStyle, Picker, Section, - Switch, + Toggle, Text, } from '@expo/ui/swift-ui'; import { background, clipShape, - disabled, frame, headerProminence, padding, @@ -25,6 +24,7 @@ import { foregroundStyle, shapes, tag, + font, } from '@expo/ui/swift-ui/modifiers'; import { useNavigation } from '@react-navigation/native'; import type { SFSymbol } from 'expo-symbols'; @@ -62,7 +62,6 @@ export default function ListScreen() { const [editModeEnabled, setEditModeEnabled] = React.useState(false); const [scrollDismissesKeyboardIndex, setScrollDismissesKeyboardIndex] = React.useState(0); const [increasedHeader, setIncreasedHeader] = React.useState(false); - const [collapsible, setCollapsible] = React.useState(false); const [customHeaderFooter, setCustomHeaderFooter] = React.useState<{ header: boolean; footer: boolean; @@ -108,61 +107,46 @@ export default function ListScreen() { deleteEnabled={deleteEnabled} selectEnabled={selectEnabled}>
- + Custom header ), - })} - footer={ - <> - {customHeaderFooter.footer && ( - - - Custom Footer - - - )} - - }> - + - - setCustomHeaderFooter((prev) => ({ ...prev, header: v }))} - /> - setCustomHeaderFooter((prev) => ({ ...prev, footer: v }))} - modifiers={[disabled(collapsible)]} + isOn={customHeaderFooter.header} + onIsOnChange={(v) => setCustomHeaderFooter((prev) => ({ ...prev, header: v }))} />
-
+
+ {/* Container Relative Frame Modifier */} +
+ + + {new Array(containerRelativeFrameCount).fill(null).map((_, i) => ( + + ))} + + +
+
@@ -736,7 +783,7 @@ function AppearSection() { return (
Hello from Popover! - This is the popover content. + This is the popover content. @@ -72,7 +72,7 @@ export default function PopoverScreen() {
- + anchor: {attachmentAnchor}, arrow: {arrowEdge} @@ -87,8 +87,8 @@ export default function PopoverScreen() { Configured Popover - Attachment: {attachmentAnchor} - Arrow Edge: {arrowEdge} + Attachment: {attachmentAnchor} + Arrow Edge: {arrowEdge} diff --git a/apps/native-component-list/src/screens/UI/ProgressScreen.ios.tsx b/apps/native-component-list/src/screens/UI/ProgressScreen.ios.tsx deleted file mode 100644 index 238cff93ca6016..00000000000000 --- a/apps/native-component-list/src/screens/UI/ProgressScreen.ios.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Host, Progress, VStack } from '@expo/ui/swift-ui'; -import * as React from 'react'; - -import { Page, Section } from '../../components/Page'; - -export default function ProgressScreen() { - const [progress, setProgress] = React.useState(0); - - React.useEffect(() => { - const interval = setInterval(() => { - setProgress((progress) => (progress + 0.05) % 1); - }, 500); - return () => { - clearInterval(interval); - }; - }, []); - - return ( - -
- - - - - - -
-
- - - - - - -
-
- - - - - - -
-
- ); -} - -ProgressScreen.navigationOptions = { - title: 'Progress', -}; diff --git a/apps/native-component-list/src/screens/UI/ProgressViewScreen.ios.tsx b/apps/native-component-list/src/screens/UI/ProgressViewScreen.ios.tsx new file mode 100644 index 00000000000000..128c0949257188 --- /dev/null +++ b/apps/native-component-list/src/screens/UI/ProgressViewScreen.ios.tsx @@ -0,0 +1,55 @@ +import { Button, Host, Form, ProgressView, Section, Text } from '@expo/ui/swift-ui'; +import { progressViewStyle, tint } from '@expo/ui/swift-ui/modifiers'; +import { useState } from 'react'; + +export default function ProgressViewScreen() { + const [progress, setProgress] = useState(0.5); + + return ( + +
+
+ +
+
+ +
+
+ + Loading... + + + {Math.round(progress * 40)}% + +
+
+ + +
+
+ + + Countdown + +
+
+
+ ); +} + +ProgressViewScreen.navigationOptions = { + title: 'ProgressView', +}; diff --git a/apps/native-component-list/src/screens/UI/RTLScreen.ios.tsx b/apps/native-component-list/src/screens/UI/RTLScreen.ios.tsx index 2108bdff1c4e65..3961ee0767ca45 100644 --- a/apps/native-component-list/src/screens/UI/RTLScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/RTLScreen.ios.tsx @@ -3,14 +3,14 @@ import { Host, HStack, Image, - Progress, + ProgressView, Picker, Slider, Switch, Text, VStack, } from '@expo/ui/swift-ui'; -import { frame, pickerStyle, tag } from '@expo/ui/swift-ui/modifiers'; +import { frame, pickerStyle, progressViewStyle, tag } from '@expo/ui/swift-ui/modifiers'; import * as React from 'react'; import { ScrollPage, Section } from '../../components/Page'; @@ -110,7 +110,7 @@ export default function RTLTestScreen() { 20% - +
diff --git a/apps/native-component-list/src/screens/UI/SectionScreen.ios.tsx b/apps/native-component-list/src/screens/UI/SectionScreen.ios.tsx new file mode 100644 index 00000000000000..0eedbe336cf6af --- /dev/null +++ b/apps/native-component-list/src/screens/UI/SectionScreen.ios.tsx @@ -0,0 +1,31 @@ +import { Host, List, Section, Text, Toggle } from '@expo/ui/swift-ui'; +import { headerProminence } from '@expo/ui/swift-ui/modifiers'; +import { useState } from 'react'; + +export default function SectionScreen() { + const [isExpanded, setIsExpanded] = useState(false); + const [increasedHeader, setIncreasedHeader] = useState(false); + + return ( + + +
+ +
+
+ This section uses the title prop + Simple and clean +
+
+
+ ); +} diff --git a/apps/native-component-list/src/screens/UI/SliderScreen.ios.tsx b/apps/native-component-list/src/screens/UI/SliderScreen.ios.tsx index 91088827561a94..ce28b774717db8 100644 --- a/apps/native-component-list/src/screens/UI/SliderScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/SliderScreen.ios.tsx @@ -78,10 +78,14 @@ export default function SliderScreen() { step={10} onValueChange={setControlledValue} /> - - +
diff --git a/apps/native-component-list/src/screens/UI/TextScreen.ios.tsx b/apps/native-component-list/src/screens/UI/TextScreen.ios.tsx index 5de39b0cf14b0d..6bcc75ed3eb9df 100644 --- a/apps/native-component-list/src/screens/UI/TextScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/TextScreen.ios.tsx @@ -1,5 +1,5 @@ import { Host, List, Text, Section } from '@expo/ui/swift-ui'; -import { font } from '@expo/ui/swift-ui/modifiers'; +import { bold, font, foregroundStyle, italic, lineLimit } from '@expo/ui/swift-ui/modifiers'; import * as React from 'react'; export default function TextScreen() { @@ -15,12 +15,30 @@ export default function TextScreen() { {textNumber}
+ Hello world {123} +
+ +
+ + Hello world! + + + Normal, italic,{' '} + bold + +
+ +
- {/* eslint-disable-next-line */} - Hello {'world'} {123} + Hello world!
+
+ + Hello world! + +
Inter Bold Font Inter Medium Font diff --git a/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx b/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx index 1babbb7b10c989..f8f0b3de612f03 100644 --- a/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx +++ b/apps/native-component-list/src/screens/UI/UIScreen.ios.tsx @@ -99,11 +99,11 @@ export const UIScreens = [ }, }, { - name: 'Progress component', - route: 'ui/progress', + name: 'ProgressView component', + route: 'ui/progress-view', options: {}, getComponent() { - return optionalRequire(() => require('./ProgressScreen')); + return optionalRequire(() => require('./ProgressViewScreen')); }, }, { @@ -114,6 +114,14 @@ export const UIScreens = [ return optionalRequire(() => require('./ListScreen')); }, }, + { + name: 'Section component', + route: 'ui/section', + options: {}, + getComponent() { + return optionalRequire(() => require('./SectionScreen')); + }, + }, { name: 'BottomSheet component', route: 'ui/bottomsheet', diff --git a/apps/native-component-list/src/screens/Video/VideoChangePlayerOutputScreen.tsx b/apps/native-component-list/src/screens/Video/VideoChangePlayerOutputScreen.tsx index 466be27a411d4c..84d269bbe6e35e 100644 --- a/apps/native-component-list/src/screens/Video/VideoChangePlayerOutputScreen.tsx +++ b/apps/native-component-list/src/screens/Video/VideoChangePlayerOutputScreen.tsx @@ -5,6 +5,7 @@ import { View, StyleSheet, Text } from 'react-native'; import { bigBuckBunnySource, elephantsDreamSource } from './videoSources'; import { styles } from './videoStyles'; import Button from '../../components/Button'; +import { E2EViewShotContainer } from '../../components/E2EViewShotContainer'; import TitledSwitch from '../../components/TitledSwitch'; const playerFactory = (player: VideoPlayer) => { @@ -19,6 +20,7 @@ export default function VideoChangePlayerOutputScreen() { // in real usage by unaware users. We want to check if expo-video gracefully handles // this by displaying the video in the last `VideoView` to use it. const [useIncorrectReplace, setUseIncorrectReplace] = useState(false); + const [nativeControls, setNativeControls] = useState(true); const player = useVideoPlayer(bigBuckBunnySource, playerFactory); const player2 = useVideoPlayer(elephantsDreamSource, playerFactory); @@ -55,18 +57,24 @@ export default function VideoChangePlayerOutputScreen() { return ( - {Array(4) - .fill(0) - .map((_, i) => ( - - ))} + + {Array(4) + .fill(0) + .map((_, i) => ( + + ))} +