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
14 changes: 14 additions & 0 deletions apps/mobile/app.config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { DEFAULT_HOSTED_APP_URL } from "@t3tools/shared/connectAuth";
import { afterEach, expect, it, vi } from "vite-plus/test";

import appleAppSiteAssociation from "../web/public/.well-known/apple-app-site-association.json";
import assetLinks from "../web/public/.well-known/assetlinks.json";

// Keep config parsing independent of local env files and build credentials.
vi.mock("../../scripts/lib/public-config.ts", () => ({
loadRepoEnv: () => ({ APP_VARIANT: process.env.APP_VARIANT }),
Expand Down Expand Up @@ -30,4 +34,14 @@ it.each([
const config = await loadConfig(value);
expect(config.ios?.bundleIdentifier).toBe(identifier);
expect(config.android?.package).toBe(identifier);
// Copied thread links open this app only while app.t3.codes lists it.
const host = new URL(DEFAULT_HOSTED_APP_URL).host;
expect(config.ios?.associatedDomains).toContain(`applinks:${host}`);
expect(appleAppSiteAssociation.applinks.details[0]?.appIDs).toContain(
`${config.ios?.appleTeamId}.${identifier}`,
);
expect(config.android?.intentFilters?.[0]?.data).toEqual([
{ scheme: "https", host, pathPrefix: "/threads/" },
]);
expect(assetLinks.map((link) => link.target.package_name)).toContain(identifier);
});
11 changes: 11 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ function resolveAppVariant(value: string | undefined): AppVariant {
}

const variant = VARIANT_CONFIG[APP_VARIANT];
// Copied thread links open the app. app.t3.codes/.well-known lists every variant's identifiers.
const HOSTED_APP_HOST = "app.t3.codes";
const iosBundleIdentifier = isIosPersonalTeamBuild
? personalTeamBundleIdentifier!
: variant.iosBundleIdentifier;
Expand Down Expand Up @@ -252,6 +254,7 @@ const config: ExpoConfig = {
associatedDomains: [
`applinks:${variant.relyingParty}`,
`webcredentials:${variant.relyingParty}`,
`applinks:${HOSTED_APP_HOST}`,
],
entitlements: {
"keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`],
Expand Down Expand Up @@ -302,6 +305,14 @@ const config: ExpoConfig = {
predictiveBackGestureEnabled: true,
// expo-sensors declares this for its pedometer, which the app does not use.
blockedPermissions: ["android.permission.ACTIVITY_RECOGNITION"],
intentFilters: [
{
action: "VIEW",
autoVerify: true,
data: [{ scheme: "https", host: HOSTED_APP_HOST, pathPrefix: "/threads/" }],
category: ["BROWSABLE", "DEFAULT"],
},
],
},
web: {
favicon: variant.assets.appIcon,
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type NavigationState,
} from "@react-navigation/native";
import { pipe } from "effect/Function";
import { DEFAULT_HOSTED_APP_URL } from "@t3tools/shared/connectAuth";

import { RegistryContext } from "@effect/atom-react";
import { ProjectChipIconRasterizer } from "./native/ProjectChipIconImages";
Expand Down Expand Up @@ -122,7 +123,13 @@ function getAppStateFromPath(path: string, options: Parameters<typeof getStateFr
}

const appLinking = {
prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],
prefixes: [
Linking.createURL("/"),
"t3code://",
"t3code-dev://",
"t3code-preview://",
DEFAULT_HOSTED_APP_URL,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
],
config: { initialRouteName: "Home" },
filter: (url: string) => shouldHandleAppLink(url, hasCloudPublicConfig()),
getInitialURL: async () => (await Linking.getInitialURL()) ?? (await launchWorkspaceURL()),
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/lib/appLinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("shouldHandleAppLink", () => {
"t3code://threads/env-1/thread-1",
"t3code://pair?pairingUrl=x",
"t3code-dev://settings/usage?tab=limits",
"https://app.t3.codes/threads/env/thread?prompt=Fix%20expo-development-client%20and%20t3code://expo-sharing",
])("handles path-bearing URL %s", (url) => {
expect(shouldHandleAppLink(url, true)).toBe(true);
});
Expand Down
8 changes: 5 additions & 3 deletions apps/mobile/src/lib/appLinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import type { NavigationState, PartialState } from "@react-navigation/native";
* to the NotFound wildcard route on every dev launch.
*/
export function shouldHandleAppLink(url: string, cloudConfigured: boolean): boolean {
// Only the route: a thread link's ?prompt= may mention either lifecycle URL.
const route = url.split(/[?#]/)[0] ?? "";
return (
!url.includes("expo-development-client") &&
!url.includes("://expo-sharing") &&
!route.includes("expo-development-client") &&
!route.includes("://expo-sharing") &&
!/^t3code(-dev|-preview)?:\/*$/.test(url) &&
(cloudConfigured || !CLOUD_ONLY_LINK_PATH.test(url.split(/[?#]/)[0] ?? ""))
(cloudConfigured || !CLOUD_ONLY_LINK_PATH.test(route))
);
}

Expand Down
14 changes: 14 additions & 0 deletions apps/web/public/.well-known/apple-app-site-association.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"applinks": {
"details": [
{
"appIDs": [
"ARK85ZXQ4Z.com.t3tools.t3code",
"ARK85ZXQ4Z.com.t3tools.t3code.preview",
"ARK85ZXQ4Z.com.t3tools.t3code.dev"
],
"components": [{ "/": "/threads/*" }]
}
]
}
}
32 changes: 32 additions & 0 deletions apps/web/public/.well-known/assetlinks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.t3tools.t3code",
"sha256_cert_fingerprints": [
"B9:F5:1E:D5:A7:6C:D1:86:C9:51:08:F2:19:91:B7:D5:3A:58:FD:A9:83:12:A9:E3:53:E2:1A:59:B2:42:03:B6"
]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.t3tools.t3code.preview",
"sha256_cert_fingerprints": [
"BE:B8:BD:67:53:2F:12:95:0A:30:62:88:91:C1:2F:D0:CF:20:28:C3:81:5D:6D:31:45:E7:5A:AF:24:A2:8F:18"
]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.t3tools.t3code.dev",
"sha256_cert_fingerprints": [
"FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"
]
}
}
]
9 changes: 8 additions & 1 deletion apps/web/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,12 @@ export const config: VercelConfig = {
dest: `${LATEST_ORIGIN}/$1`,
},
],
rewrites: [routes.rewrite("/(.*)", "/index.html")],
rewrites: [
// Apple fetches it without an extension; the .json file is served as application/json.
routes.rewrite(
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
"/.well-known/apple-app-site-association",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
"/.well-known/apple-app-site-association.json",
),
routes.rewrite("/(.*)", "/index.html"),
],
};
7 changes: 4 additions & 3 deletions docs/user/prompt-links.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ malicious.
## Share a thread

**Copy thread** copies a link such as `https://app.t3.codes/threads/<environment>/<thread>`. Paste
it anywhere, such as Slack, a GitHub comment, or your notes. Opening it asks where to open it, like
a prompt link, then opens the thread. Only someone connected to the machine that runs the thread
can see it. In T3 Code, a thread link in a message opens the thread in the app.
it anywhere, such as Slack, a GitHub comment, or your notes. On a phone with T3 Code installed, it
opens the thread in the app. Elsewhere, opening it asks where to open it, like a prompt link, then
opens the thread. Only someone connected to the machine that runs the thread can see it. In T3 Code,
a thread link in a message opens the thread in the app.
Loading