Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/workflows/mobile-showcase-screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Mobile Showcase Screenshots

on:
workflow_dispatch:
inputs:
platform:
description: Device platforms to capture
required: true
default: all
type: choice
options:
- all
- ios
- android

permissions:
contents: read

env:
NODE_OPTIONS: --max-old-space-size=8192

jobs:
ios:
name: iPhone 6.9, iPhone 6.5, and iPad 13
if: inputs.platform == 'all' || inputs.platform == 'ios'
runs-on: blacksmith-12vcpu-macos-26
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true

- name: Expose pnpm
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version

- name: Capture iOS showcase
run: pnpm screenshots:mobile --platform ios

- name: Validate App Store Connect assets
run: pnpm screenshots:mobile --platform ios --validate-only

- name: Upload iOS screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: app-store-connect-screenshots
path: artifacts/app-store/screenshots/apple/
if-no-files-found: warn
retention-days: 14

android:
name: Android phone, 7-inch tablet, and 10-inch tablet
if: inputs.platform == 'all' || inputs.platform == 'android'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
env:
T3_SHOWCASE_ANDROID_ABI: x86_64
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
with:
node-version-file: package.json
cache: true
run-install: true

- name: Expose pnpm
run: |
pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
"$vp_pnpm_bin/pnpm" --version

- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17

- name: Setup Gradle cache
uses: gradle/actions/setup-gradle@v5

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Capture Android showcase
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 36
target: google_apis
arch: x86_64
profile: pixel_7_pro
avd-name: Pixel_10_Pro
cores: 8
ram-size: 4096M
disable-animations: false
script: pnpm screenshots:mobile --platform android

- name: Validate Google Play assets
run: pnpm screenshots:mobile --platform android --validate-only

- name: Upload Android screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: google-play-screenshots
path: artifacts/app-store/screenshots/google-play/
if-no-files-found: warn
retention-days: 14
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ squashfs-root/
.gstack/
dist-electron/
.electron-runtime/
.showcase/
apps/mobile/.showcase/
artifacts/app-store/screenshots/
node_modules/
.alchemy/
*.log
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { registerRootComponent } from "expo";
import "react-native-gesture-handler";
import { LogBox } from "react-native";
import { featureFlags } from "react-native-screens";

import App from "./src/App";
Expand All @@ -8,4 +9,8 @@ import App from "./src/App";
// native stack is rendered inside a non-fitToContents formSheet.
featureFlags.experiment.synchronousScreenUpdatesEnabled = true;

if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
LogBox.ignoreAllLogs();
}

registerRootComponent(App);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ class T3NativeControlsModule : Module() {
override fun definition() = ModuleDefinition {
Name("T3NativeControls")

Function("getShowcasePairingUrl") {
appContext.currentActivity?.intent?.getStringExtra("showcasePairingUrl")
}

Function("getShowcaseScene") {
val storedScene = appContext.reactContext
?.filesDir
?.resolve("t3-showcase-scene")
?.takeIf { it.isFile }
?.readText()
?.trim()
?.takeIf { it.isNotEmpty() }
storedScene ?: appContext.currentActivity?.intent?.getStringExtra("showcaseScene")
}

Function("prepareShowcaseCapture") {
// Android app data is cleared by the host runner before launch.
}

Function("markShowcaseReady") { scene: String ->
appContext.reactContext
?.filesDir
?.resolve("t3-showcase-ready")
?.writeText(scene)
}

View(T3HeaderButtonView::class) {
Prop("label") { view: T3HeaderButtonView, label: String ->
view.setLabel(label)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,48 @@
import ExpoModulesCore
import Security

public final class T3NativeControlsModule: Module {
public func definition() -> ModuleDefinition {
Name("T3NativeControls")

Function("getShowcasePairingUrl") {
let arguments = ProcessInfo.processInfo.arguments
guard
let flagIndex = arguments.firstIndex(of: "--showcasePairingUrl"),
arguments.indices.contains(flagIndex + 1)
else {
return nil as String?
}
return arguments[flagIndex + 1]
}

Function("getShowcaseScene") { () -> String? in
let scenePath = NSHomeDirectory() + "/Library/Caches/T3ShowcaseScene"
if let storedScene = try? String(contentsOfFile: scenePath, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines), !storedScene.isEmpty {
return storedScene
}
let arguments = ProcessInfo.processInfo.arguments
guard
let flagIndex = arguments.firstIndex(of: "--showcaseScene"),
arguments.indices.contains(flagIndex + 1)
else {
return nil as String?
}
return arguments[flagIndex + 1]
}

Function("prepareShowcaseCapture") {
for itemClass in [kSecClassGenericPassword, kSecClassInternetPassword] {
SecItemDelete([kSecClass as String: itemClass] as CFDictionary)
}
}

Function("markShowcaseReady") { (scene: String) in
let readyPath = NSHomeDirectory() + "/Library/Caches/T3ShowcaseReadyScene"
try? scene.write(toFile: readyPath, atomically: true, encoding: .utf8)
}

View(T3HeaderButtonView.self) {
Prop("label") { (view: T3HeaderButtonView, label: String) in
view.setLabel(label)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class T3TerminalModule : Module() {
view.focusRequest = focusRequest
}

Prop("autoFocus") { view: T3TerminalView, autoFocus: Boolean ->
view.autoFocus = autoFocus
}

Prop("appearanceScheme") { view: T3TerminalView, appearanceScheme: String ->
view.appearanceScheme = appearanceScheme
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex
}
}

var autoFocus: Boolean = true
set(value) {
field = value
if (value) {
requestKeyboardFocus()
} else {
inputView.clearFocus()
hideKeyboard()
}
}

var backgroundColorHex: String = "#24292E"
set(value) {
field = value
Expand Down Expand Up @@ -368,6 +379,13 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex
inputMethodManager?.showSoftInput(inputView, InputMethodManager.SHOW_IMPLICIT)
}

private fun hideKeyboard() {
val inputMethodManager = context.getSystemService(
Context.INPUT_METHOD_SERVICE
) as? InputMethodManager
inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
}

private fun applyTheme() {
setBackgroundColor(backgroundColorValue)
container.setBackgroundColor(backgroundColorValue)
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public class T3TerminalModule: Module {
view.focusRequest = focusRequest
}

Prop("autoFocus") { (view: T3TerminalView, autoFocus: Bool) in
view.autoFocus = autoFocus
}

Prop("appearanceScheme") { (view: T3TerminalView, appearanceScheme: String) in
view.appearanceScheme = appearanceScheme
}
Expand Down
13 changes: 12 additions & 1 deletion apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,17 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
}
}

var autoFocus = true {
didSet {
guard oldValue != autoFocus else { return }
if autoFocus {
requestKeyboardFocus()
} else {
inputField.resignFirstResponder()
}
}
}

var appearanceScheme: String = TerminalAppearanceScheme.dark.rawValue {
didSet {
guard oldValue != appearanceScheme else { return }
Expand Down Expand Up @@ -359,7 +370,7 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
public override func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else { return }
guard window != nil, autoFocus else { return }
DispatchQueue.main.async { [weak self] in
self?.requestKeyboardFocus()
}
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"start:dev": "APP_VARIANT=development expo start",
"start:preview": "APP_VARIANT=preview expo start",
"start:prod": "APP_VARIANT=production expo start",
"showcase": "APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code-dev --clear",
"screenshots": "node ../../scripts/mobile-showcase.ts",
"android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",
"android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android",
"android:preview": "APP_VARIANT=preview EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android",
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createStaticNavigation, DarkTheme, DefaultTheme } from "@react-navigati
import { RegistryContext } from "@effect/atom-react";
import { ConfirmDialogHost } from "./components/ConfirmDialogHost";
import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";
import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene";
import { IncomingShareProvider } from "./features/sharing/IncomingShareProvider";
import { AppearancePreferencesProvider } from "./features/settings/appearance/AppearancePreferencesProvider";
import { RootStack } from "./Stack";
Expand All @@ -21,6 +22,10 @@ import { useThemeColor } from "./lib/useThemeColor";

import "../global.css";

if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
prepareNativeShowcaseCapture();
}

const appLinking = {
prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],
// The Expo dev client launches the app via
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnv
import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen";
import { SettingsWaitlistRouteScreen } from "./features/settings/SettingsWaitlistRouteScreen";
import { ShowcaseCaptureCoordinator } from "./features/showcase/ShowcaseCaptureCoordinator";
import {
SettingsLegalDocumentCloseHeaderButton,
SettingsLegalDocumentExternalHeaderButton,
Expand Down Expand Up @@ -313,6 +314,7 @@ function RootStackLayout(props: {

return (
<HardwareKeyboardCommandProvider pathname={pathname}>
<ShowcaseCaptureCoordinator pathname={pathname} />
<ClerkSettingsSheetDetentProvider initiallyExpanded={false}>
<AdaptiveWorkspaceLayout pathname={workspacePathname}>
{props.children}
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/features/connection/CloudEnvironmentRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { type RelayEnvironmentView, useConnectionController } from "./useConnect
export function CloudEnvironmentRows(props: {
readonly connectedCloudEnvironments: ReadonlyArray<ConnectedEnvironmentSummary>;
readonly onReconnectEnvironment: (environmentId: EnvironmentId) => void;
readonly showcaseAvailableEnvironments?: ReadonlyArray<RelayEnvironmentView>;
readonly showcaseSignedIn?: boolean;
/**
* Hide the "T3 Connect" section title + refresh button for hosts that
* provide their own chrome (the onboarding sheet's native header and
Expand All @@ -43,7 +45,8 @@ export function CloudEnvironmentRows(props: {
const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false });
const controller = useConnectionController();
const iconColor = useThemeColor("--color-icon");
const availableCloudEnvironments = controller.availableRelayEnvironments;
const availableCloudEnvironments =
props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments;
const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);
const hasCloudRows =
props.connectedCloudEnvironments.length > 0 || availableCloudEnvironments.length > 0;
Expand All @@ -64,7 +67,7 @@ export function CloudEnvironmentRows(props: {

const showHeader = props.showHeader ?? true;

if (!isSignedIn) return null;
if (!(props.showcaseSignedIn ?? isSignedIn)) return null;

return (
<View collapsable={false} className={cn("gap-3", showHeader && "mt-5")}>
Expand Down
Loading
Loading