Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const WSL_FORWARDED_ENV_NAMES = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"T3CODE_TELEMETRY_ENABLED",
"T3CODE_GITHUB_APP_CLIENT_ID",
// Otherwise the WSL server keeps exporting to endpoints from the bootstrap.
"T3CODE_OTEL_SDK_DISABLED",
"OTEL_SDK_DISABLED",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/shell/DesktopShellEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const LOGIN_SHELL_ENV_NAMES = [
"SSL_CERT_DIR",
"NODE_EXTRA_CA_CERTS",
"T3CODE_TELEMETRY_ENABLED",
"T3CODE_GITHUB_APP_CLIENT_ID",
"__MISE_DIFF",
] as const;
const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const;
Expand Down Expand Up @@ -450,6 +451,7 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix
"SSL_CERT_DIR",
"NODE_EXTRA_CA_CERTS",
"T3CODE_TELEMETRY_ENABLED",
"T3CODE_GITHUB_APP_CLIENT_ID",
] as const) {
if (!config.env[name] && shellEnvironment[name]) {
config.env[name] = shellEnvironment[name];
Expand Down
16 changes: 16 additions & 0 deletions apps/mobile/src/features/projects/AddProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import { sourceControlClients } from "@t3tools/client-runtime/source-control-cli
import { ThemedSwitch } from "../../components/ui/ThemedSwitch";
import { uuidv4 } from "../../lib/uuid";
import { useAtomCommand } from "../../state/use-atom-command";
import { suggestAppAccessForRemote } from "./suggestAppAccess";
import { useAtomQueryRunner } from "../../state/use-atom-query-runner";
import {
useRemoteConnectionStatus,
Expand Down Expand Up @@ -997,6 +998,10 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s
);
const environment = resolveAddProjectEnvironment(environmentOptions, selectedEnvironmentId);
const createNew = useAtomCommand(projectEnvironment.createNew, { reportFailure: false });
const checkPublishedAppAccess = useAtomCommand(sourceControlEnvironment.checkAppAccess, {
reportFailure: false,
reportDefect: false,
});
const publishRepository = useAtomCommand(sourceControlEnvironment.publishRepository, {
reportFailure: false,
});
Expand Down Expand Up @@ -1095,6 +1100,12 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s
`Could not create the ${publishTarget.definition.label} repository`,
errorMessage(Cause.squash(publishResult.cause)),
);
} else {
void suggestAppAccessForRemote(
checkPublishedAppAccess,
environment.environmentId,
publishResult.value.remoteUrl,
);
}
});
}
Expand Down Expand Up @@ -1315,6 +1326,10 @@ export function AddProjectDestinationScreen(props: {
const startProjectClone = useAtomCommand(sourceControlEnvironment.startProjectClone, {
reportFailure: false,
});
const checkAppAccess = useAtomCommand(sourceControlEnvironment.checkAppAccess, {
reportFailure: false,
reportDefect: false,
});
const navigation = useNavigation();
const environment = useEnvironmentFromParam(props.environmentId);
const canWriteSourceControl = useEnvironmentScope(
Expand Down Expand Up @@ -1376,6 +1391,7 @@ export function AddProjectDestinationScreen(props: {
if (AsyncResult.isFailure(startResult)) {
setError(errorMessage(Cause.squash(startResult.cause)));
} else {
void suggestAppAccessForRemote(checkAppAccess, environment.environmentId, remoteUrl);
// Wait for the create event; the client store otherwise falls back to the project picker and loses clone controls.
const project = await waitForProject(
{ environmentId: environment.environmentId, projectId },
Expand Down
38 changes: 38 additions & 0 deletions apps/mobile/src/features/projects/suggestAppAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {
EnvironmentId,
SourceControlAppAccess,
SourceControlAppAccessInput,
} from "@t3tools/contracts";
import { appSignInTargetForRemote } from "@t3tools/client-runtime/source-control-clients";
import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime";
import { Alert, Linking } from "react-native";

/**
* After a clone, suggests installing the forge app on the repository's owner when the server is
* signed in with it but the app does not reach the repository.
*/
export async function suggestAppAccessForRemote(
check: (value: {
readonly environmentId: EnvironmentId;
readonly input: SourceControlAppAccessInput;
}) => Promise<AtomCommandResult<SourceControlAppAccess, unknown>>,
environmentId: EnvironmentId,
remoteUrl: string,
): Promise<void> {
const target = appSignInTargetForRemote(remoteUrl);
if (target === null) return;
const result = await check({ environmentId, input: target.input });
if (result._tag !== "Success" || result.value.status !== "not-covered") return;
const owner = target.input.repository.split("/")[0] ?? target.input.repository;
Alert.alert(
`Give ${target.appSignIn.appName} access to ${owner}`,
`It isn't installed there, so GitHub requests for ${target.input.repository} use your own login and its lower rate limit.`,
[
{ text: "Not now", style: "cancel" },
{
text: "Install",
onPress: () => void Linking.openURL(target.appSignIn.installUrl).catch(() => undefined),
},
],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import { pullRequestLabelColor } from "./pullRequestList";
import { PullRequestReactions } from "./PullRequestReactions";
import { composerPullRequests } from "../../state/pull-requests";
import { useEnvironmentQuery } from "../../state/query";
import { SourceControlAppAccessNotice } from "./SourceControlAppAccessNotice";
import { usePullToRefresh } from "../../state/use-pull-to-refresh";
import {
resolveNativeReviewDiffView,
Expand Down Expand Up @@ -720,6 +721,12 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
<View>
{tab === "overview" && identity ? (
<View>
<SourceControlAppAccessNotice
environmentId={params.environmentId}
provider={identity.provider}
host={params.host}
repository={params.repository}
/>
{pr ? (
<PullRequestOverview
pr={pr}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { EnvironmentId } from "@t3tools/contracts";
import {
readHostCredentialSettings,
sourceControlClients,
} from "@t3tools/client-runtime/source-control-clients";
import { Linking, Pressable, View } from "react-native";

import { AppText as Text } from "../../components/ui/AppText";
import { useEnvironmentPresentation } from "../../state/presentation";
import { useEnvironmentQuery } from "../../state/query";
import { sourceControlEnvironment } from "../../state/sourceControl";

/**
* Says when the server's forge app sign-in cannot reach `repository`, so requests there fall back
* to the user's own login. Renders nothing for servers that do not use the app.
*/
export function SourceControlAppAccessNotice(props: {
readonly environmentId: string;
readonly provider: string | undefined;
readonly host: string | undefined;
/** `owner/name`. */
readonly repository: string;
}) {
const environmentId = EnvironmentId.make(props.environmentId);
const { presentation } = useEnvironmentPresentation(environmentId);
const definition = props.provider ? sourceControlClients.find(props.provider) : undefined;
const appSignIn = definition?.credentials?.appSignIn;
const host = (props.host ?? definition?.publicHost ?? "").toLowerCase();
const settings = presentation?.serverConfig?.settings;
const signedIn =
definition !== undefined &&
settings !== undefined &&
(readHostCredentialSettings(settings, definition.kind).signIns[host] ?? "").length > 0;
const access = useEnvironmentQuery(
definition !== undefined && appSignIn?.hosts.includes(host) && signedIn
? sourceControlEnvironment.appAccess({
environmentId,
input: { provider: definition.kind, host, repository: props.repository },
})
: null,
);
if (appSignIn === undefined || access.data?.status !== "not-covered") return null;
const owner = props.repository.split("/")[0] ?? props.repository;

return (
<View className="mb-4 gap-1 rounded-2xl bg-grouped-card px-4 py-3">
<Text className="text-sm text-foreground-muted">
{appSignIn.appName} isn't installed on {owner}, so requests here use your own GitHub login
and its lower rate limit.
</Text>
<Pressable
accessibilityRole="link"
onPress={() => void Linking.openURL(appSignIn.installUrl).catch(() => undefined)}
>
<Text className="text-sm font-t3-bold text-foreground">Install on {owner}</Text>
</Pressable>
</View>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ vi.mock("../../state/preferences", () => ({
updateMobilePreferencesAtom: {},
}));
vi.mock("./components/AutoSettleDaysField", () => ({ AutoSettleDaysField: () => null }));
vi.mock("./components/SourceControlSignInSection", () => ({
SourceControlSignInSection: () => null,
}));
vi.mock("react-native-safe-area-context", () => ({ useSafeAreaInsets: () => ({ bottom: 0 }) }));
vi.mock("@react-navigation/native", () => ({ useNavigation: () => ({}) }));
vi.mock("../../state/session", () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { DatabaseStorageNotice } from "./components/DatabaseStorageNotice";
import { ListChoiceRow as SettingsChoiceRow } from "../../components/ui/ListChoiceRow";
import { SettingsSection } from "./components/SettingsSection";
import { SettingsSwitchRow } from "./components/SettingsSwitchRow";
import { SourceControlSignInSection } from "./components/SourceControlSignInSection";
import { SettingsProjectOverridesSection } from "./components/SettingsProjectOverridesSection";
import { useSettingsEnvironmentFilter } from "./settings-environment-filter";
import { useSettingsWriter } from "./useSettingsWriter";
Expand Down Expand Up @@ -382,6 +383,12 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) {

{props.page === "source-control" ? (
<>
{projectSelected ? null : (
<SourceControlSignInSection
environments={selectedTargets}
canWrite={(environmentId) => writableEnvironments.has(environmentId)}
/>
)}
<SettingsSection title="Pull requests">
<SettingsSwitchRow
icon={PullRequestGlyph.merged}
Expand Down
Loading
Loading