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
24 changes: 24 additions & 0 deletions apps/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ Build and run the local iOS dev client:
vp run ios:dev
```

If your Xcode account only has a Personal Team, use a bundle identifier you control and opt into the
reduced-capability local build. Personal Team builds omit the widget extension, push entitlement, and
native Sign in with Apple entitlement; builds without this opt-in are unchanged.

```bash
T3CODE_IOS_PERSONAL_TEAM=1 \
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID=com.example.t3code.dev \
vp run ios:dev
```

Build and install a self-contained Release app that does not need Metro:

```bash
vp run ios:release
```

The Personal Team equivalent also needs a unique bundle identifier:

```bash
T3CODE_IOS_PERSONAL_TEAM=1 \
T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID=com.example.t3code \
vp run ios:release
```

Build and run the local iOS preview app:

```bash
Expand Down
61 changes: 35 additions & 26 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ const repoEnv = loadRepoEnv();
Object.assign(process.env, repoEnv);

const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT);
const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1";

const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim();

if (isIosPersonalTeamBuild && !personalTeamBundleIdentifier) {
throw new Error(
"T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID is required when T3CODE_IOS_PERSONAL_TEAM=1.",
);
}

const VARIANT_CONFIG: Record<
AppVariant,
Expand All @@ -17,7 +26,6 @@ const VARIANT_CONFIG: Record<
readonly iosIcon: string;
readonly iosBundleIdentifier: string;
readonly androidPackage: string;
readonly relyingParty?: string;
}
> = {
development: {
Expand All @@ -26,23 +34,20 @@ const VARIANT_CONFIG: Record<
iosIcon: "./assets/icon-composer-dev.icon",
iosBundleIdentifier: "com.t3tools.t3code.dev",
androidPackage: "com.t3tools.t3code.dev",
relyingParty: "clerk.t3.codes",
},
preview: {
appName: "T3 Code Preview",
scheme: "t3code-preview",
iosIcon: "./assets/icon-composer-prod.icon",
iosBundleIdentifier: "com.t3tools.t3code.preview",
androidPackage: "com.t3tools.t3code.preview",
relyingParty: "clerk.t3.codes",
},
production: {
appName: "T3 Code",
scheme: "t3code",
iosIcon: "./assets/icon-composer-prod.icon",
iosBundleIdentifier: "com.t3tools.t3code",
androidPackage: "com.t3tools.t3code",
relyingParty: "clerk.t3.codes",
},
};

Expand All @@ -58,6 +63,27 @@ function resolveAppVariant(value: string | undefined): AppVariant {
}

const variant = VARIANT_CONFIG[APP_VARIANT];
const iosBundleIdentifier =
isIosPersonalTeamBuild && personalTeamBundleIdentifier
? personalTeamBundleIdentifier
: variant.iosBundleIdentifier;

const widgetsPlugin = [
"expo-widgets",
{
bundleIdentifier: `${iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${iosBundleIdentifier}`,
enablePushNotifications: true,
widgets: [
{
name: "AgentActivity",
displayName: "Agent Activity",
description: "Shows the current state of active T3 Code agents.",
supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"],
},
],
},
] satisfies NonNullable<ExpoConfig["plugins"]>[number];

const config: ExpoConfig = {
name: variant.appName,
Expand All @@ -80,11 +106,7 @@ const config: ExpoConfig = {
ios: {
icon: variant.iosIcon,
supportsTablet: true,
bundleIdentifier: variant.iosBundleIdentifier,
associatedDomains: [
`applinks:${variant.relyingParty}`,
`webcredentials:${variant.relyingParty}`,
],
bundleIdentifier: iosBundleIdentifier,
infoPlist: {
NSAppTransportSecurity: {
NSAllowsArbitraryLoads: true,
Expand All @@ -110,9 +132,10 @@ const config: ExpoConfig = {
},
plugins: [
"expo-router",
"expo-asset",
"expo-font",
"expo-secure-store",
["@clerk/expo", { theme: "./clerk-theme.json" }],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS associated domains removed

High Severity

Removing relyingParty from the variant config inadvertently dropped the associatedDomains block from the iOS configuration. This prevents standard iOS builds from declaring Universal Links and webcredentials for the Clerk relying party.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 276b453. Configure here.

["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }],
"expo-web-browser",
[
"expo-camera",
Expand Down Expand Up @@ -148,24 +171,10 @@ const config: ExpoConfig = {
},
],
"./plugins/withIosCocoaPodsUuidCache.cjs",
[
"expo-widgets",
{
bundleIdentifier: `${variant.iosBundleIdentifier}.widgets`,
groupIdentifier: `group.${variant.iosBundleIdentifier}`,
enablePushNotifications: true,
widgets: [
{
name: "AgentActivity",
displayName: "Agent Activity",
description: "Shows the current state of active T3 Code agents.",
supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"],
},
],
},
],
...(!isIosPersonalTeamBuild ? [widgetsPlugin] : []),
"./plugins/withIosSceneLifecycle.cjs",
"./plugins/withAndroidCleartextTraffic.cjs",
...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),
],
extra: {
appVariant: APP_VARIANT,
Expand Down
9 changes: 9 additions & 0 deletions apps/mobile/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { withUniwindConfig } = require("uniwind/metro");
/** @type {import("expo/metro-config").MetroConfig} */
const config = getDefaultConfig(__dirname);
const workspaceRoot = path.resolve(__dirname, "../..");
const escapedWorkspaceRoot = workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const mobileShikiRoot = path.dirname(require.resolve("shiki/package.json", { paths: [__dirname] }));
const resolveShikiDependencyRoot = (packageName) => {
const entryPath = require.resolve(packageName, { paths: [mobileShikiRoot] });
Expand All @@ -25,6 +26,14 @@ const resolveShikiDependencyRoot = (packageName) => {
config.watchFolders = [...new Set([...(config.watchFolders ?? []), workspaceRoot])];
config.resolver = {
...config.resolver,
blockList: [
...(Array.isArray(config.resolver?.blockList)
? config.resolver.blockList
: config.resolver?.blockList
? [config.resolver.blockList]
: []),
new RegExp(`${escapedWorkspaceRoot}[/\\\\]\\.t3[/\\\\].*`),
],
extraNodeModules: {
// oxlint-disable-next-line unicorn/no-useless-fallback-in-spread
...(config.resolver?.extraNodeModules ?? {}),
Expand Down
19 changes: 19 additions & 0 deletions apps/mobile/modules/t3-composer-editor/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'

group = 'com.t3tools.composereditor'
version = '0.0.0'

android {
namespace 'expo.modules.t3composereditor'
compileSdk rootProject.ext.compileSdkVersion

defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}

dependencies {
implementation project(':expo-modules-core')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package expo.modules.t3composereditor

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class T3ComposerEditorModule : Module() {
override fun definition() = ModuleDefinition {
Name("T3ComposerEditor")

View(T3ComposerEditorView::class) {
Prop("controlledDocumentJson") { view: T3ComposerEditorView, documentJson: String ->
view.setControlledDocumentJson(documentJson)
}
Prop("themeJson") { view: T3ComposerEditorView, themeJson: String ->
view.setThemeJson(themeJson)
}
Prop("placeholder") { view: T3ComposerEditorView, placeholder: String ->
view.setPlaceholder(placeholder)
}
Prop("fontFamily") { view: T3ComposerEditorView, fontFamily: String ->
view.setFontFamily(fontFamily)
}
Prop("fontSize") { view: T3ComposerEditorView, fontSize: Double ->
view.setFontSize(fontSize.toFloat())
}
Prop("lineHeight") { view: T3ComposerEditorView, lineHeight: Double ->
view.setLineHeight(lineHeight.toFloat())
}
Prop("contentInsetVertical") { view: T3ComposerEditorView, contentInsetVertical: Double ->
view.setContentInsetVertical(contentInsetVertical.toInt())
}
Prop("editable") { view: T3ComposerEditorView, editable: Boolean ->
view.setEditable(editable)
}
Prop("scrollEnabled") { view: T3ComposerEditorView, scrollEnabled: Boolean ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium t3composereditor/T3ComposerEditorModule.kt:35

The scrollEnabled prop does not actually disable scrolling. setScrollEnabled only toggles editor.isVerticalScrollBarEnabled, which controls whether the scrollbar is drawn — not whether the view scrolls. When scrollEnabled={false} is passed from JS, the editor remains scrollable and only the scrollbar disappears, so the prop silently does not work. Consider disabling touch interception or overriding onTouchEvent to actually prevent scrolling when scrollEnabled is false.

Also found in 1 other location(s)

apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt:199

setScrollEnabled() at line 199 only assigns editor.isVerticalScrollBarEnabled. Android's setVerticalScrollBarEnabled() controls whether the scrollbar is drawn, not whether an EditText can actually scroll. When callers pass scrollEnabled=false, long composer contents can still be vertically scrolled; only the scrollbar disappears, so the exposed prop does not work on Android.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt around line 35:

The `scrollEnabled` prop does not actually disable scrolling. `setScrollEnabled` only toggles `editor.isVerticalScrollBarEnabled`, which controls whether the scrollbar is drawn — not whether the view scrolls. When `scrollEnabled={false}` is passed from JS, the editor remains scrollable and only the scrollbar disappears, so the prop silently does not work. Consider disabling touch interception or overriding `onTouchEvent` to actually prevent scrolling when `scrollEnabled` is false.

Also found in 1 other location(s):
- apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorView.kt:199 -- `setScrollEnabled()` at line `199` only assigns `editor.isVerticalScrollBarEnabled`. Android's `setVerticalScrollBarEnabled()` controls whether the scrollbar is drawn, not whether an `EditText` can actually scroll. When callers pass `scrollEnabled=false`, long composer contents can still be vertically scrolled; only the scrollbar disappears, so the exposed prop does not work on Android.

view.setScrollEnabled(scrollEnabled)
}
Prop("autoFocus") { view: T3ComposerEditorView, autoFocus: Boolean ->
view.setAutoFocus(autoFocus)
}
Prop("autoCorrect") { view: T3ComposerEditorView, autoCorrect: Boolean ->
view.setAutoCorrect(autoCorrect)
}
Prop("spellCheck") { view: T3ComposerEditorView, spellCheck: Boolean ->
view.setSpellCheck(spellCheck)
}

Events(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium t3composereditor/T3ComposerEditorModule.kt:48

The Events(...) declaration omits onComposerSubmit, and T3ComposerEditorView defines no corresponding EventDispatcher or hardware-keyboard handler. As a result, the onSubmit prop exposed by ComposerEditorProps is never invoked on Android — hardware-keyboard submit that works on iOS silently does nothing here. Consider adding "onComposerSubmit" to the Events list, wiring up an onComposerSubmit dispatcher in the view, and detecting the submit key combination (e.g., Enter without Shift) to fire it.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ComposerEditorModule.kt around line 48:

The `Events(...)` declaration omits `onComposerSubmit`, and `T3ComposerEditorView` defines no corresponding `EventDispatcher` or hardware-keyboard handler. As a result, the `onSubmit` prop exposed by `ComposerEditorProps` is never invoked on Android — hardware-keyboard submit that works on iOS silently does nothing here. Consider adding `"onComposerSubmit"` to the `Events` list, wiring up an `onComposerSubmit` dispatcher in the view, and detecting the submit key combination (e.g., Enter without Shift) to fire it.

"onComposerChange",
"onComposerSelectionChange",
"onComposerFocus",
"onComposerBlur",
"onComposerPasteImages",
"onComposerContentSizeChange",
)

AsyncFunction("focus") { view: T3ComposerEditorView ->
view.focusEditor()
}
AsyncFunction("blur") { view: T3ComposerEditorView ->
view.blurEditor()
}
AsyncFunction("setSelection") { view: T3ComposerEditorView, start: Int, end: Int ->
view.setSelection(start, end)
}
}
}
}
Loading
Loading