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
6 changes: 6 additions & 0 deletions apps/desktop/build/entitlements.mac.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:folo.is</string>
<string>applinks:app.folo.is</string>
<string>applinks:dev.folo.is</string>
</array>
</dict>
</plist>
6 changes: 6 additions & 0 deletions apps/desktop/build/entitlements.mas.plist
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:folo.is</string>
<string>applinks:app.folo.is</string>
<string>applinks:dev.folo.is</string>
</array>
</dict>
</plist>
1 change: 1 addition & 0 deletions apps/desktop/forge.config.cts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
prune: false,
extendInfo: {
ITSAppUsesNonExemptEncryption: false,
NSUserActivityTypes: ["NSUserActivityTypeBrowsingWeb"],
},
osxSign: {
optionsForFile:
Expand Down Expand Up @@ -268,7 +269,7 @@
files: [],
}
let basePath = ""
makeResults = makeResults.map((result) => {

Check warning on line 272 in apps/desktop/forge.config.cts

View workflow job for this annotation

GitHub Actions / Format, Lint and Typecheck (lts/*)

Assignment to function parameter 'makeResults'
result.artifacts = result.artifacts
.map((artifact) => {
if (artifactRegex.test(artifact)) {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/layer/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<link rel="canonical" href="https://app.folo.is" />

<!-- Apple Meta Tags -->
<meta name="apple-itunes-app" content="app-id=6739802604" />
<meta name="apple-itunes-app" content="app-id=6739802604, app-argument=folo://" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Folo" />
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/ios/Folo/Folo.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
<string>Default</string>
</array>
</dict>
</plist>
</plist>
169 changes: 124 additions & 45 deletions apps/mobile/src/hooks/useIntentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ import { useEffect } from "react"
import { useNavigation } from "../lib/navigation/hooks"
import { FollowScreen } from "../screens/(modal)/FollowScreen"

const SUPPORTED_WEB_HOSTS = new Set([
"app.folo.is",
"dev.folo.is",
"folo.is",
"www.folo.is",
"app.follow.is",
"dev.follow.is",
"follow.is",
])

type DeepLinkParams = {
id: string | null
type: string | null
url?: string | null
view?: string | null
}

// This needs to stay outside of react to persist between account switches
let previousIntentUrl = ""
export const resetIntentUrl = () => {
Expand Down Expand Up @@ -46,59 +63,121 @@ export function useIntentHandler() {
// follow://add?type=url&url=rsshub://rsshub/routes/en
// follow://list?id=60580187699502080
// follow://feed?id=60580187699502080&view=1
// https://app.folo.is/share/feeds/60580187699502080
// https://app.folo.is/share/lists/60580187699502080
// https://app.folo.is/add?type=feed&id=60580187699502080
const extractParamsFromDeepLink = (
incomingUrl: string | null,
):
| { id: string | null; type: string | null; url?: string | null; view?: string | null }
| "refresh"
| null => {
): DeepLinkParams | "refresh" | null => {
if (!incomingUrl) return null

try {
const url = new URL(incomingUrl)
if (url.protocol !== "follow:" && url.protocol !== "folo:") return null

switch (url.hostname) {
case "add": {
const { searchParams } = url
if (!searchParams.has("id") && !searchParams.has("url")) return null

return {
id: searchParams.get("id"),
type: searchParams.get("type"),
url: searchParams.get("url"),
view: searchParams.get("view"),
}
}
case "list": {
const { searchParams } = url
if (!searchParams.has("id") && !searchParams.has("url")) return null

return {
id: searchParams.get("id"),
type: "list",
view: searchParams.get("view"),
}
}
case "feed": {
const { searchParams } = url
if (!searchParams.has("id") && !searchParams.has("url")) return null

return {
id: searchParams.get("id"),
type: "feed",
url: searchParams.get("url"),
view: searchParams.get("view"),
}
}
case "refresh": {
return "refresh"
}
default: {
return null
}

if (url.protocol === "follow:" || url.protocol === "folo:") {
return extractParamsFromSchemeUrl(url)
}

if (
(url.protocol === "https:" || url.protocol === "http:") &&
SUPPORTED_WEB_HOSTS.has(url.hostname)
) {
return extractParamsFromWebUrl(url)
}

return null
} catch {
return null
}
}

const extractParamsFromSchemeUrl = (url: URL): DeepLinkParams | "refresh" | null => {
switch (url.hostname) {
case "add": {
return extractParamsFromAddSearchParams(url.searchParams)
}
case "list": {
const { searchParams } = url
if (!searchParams.has("id") && !searchParams.has("url")) return null

return {
id: searchParams.get("id"),
type: "list",
view: searchParams.get("view"),
}
}
case "feed": {
const { searchParams } = url
if (!searchParams.has("id") && !searchParams.has("url")) return null

return {
id: searchParams.get("id"),
type: "feed",
url: searchParams.get("url"),
view: searchParams.get("view"),
}
}
case "refresh": {
return "refresh"
}
default: {
return null
}
}
}

const extractParamsFromWebUrl = (url: URL): DeepLinkParams | "refresh" | null => {
const pathname = normalizePathname(url.pathname)

if (pathname === "/refresh") {
return "refresh"
}

if (pathname === "/add") {
return extractParamsFromAddSearchParams(url.searchParams)
}

const feedMatch = pathname.match(/^\/(?:share\/feeds|feed)\/([^/]+)$/)
if (feedMatch) {
const feedId = feedMatch[1]
if (!feedId) return null

return {
id: decodeURIComponent(feedId),
type: "feed",
view: url.searchParams.get("view"),
}
}

const listMatch = pathname.match(/^\/(?:share\/lists|list)\/([^/]+)$/)
if (listMatch) {
const listId = listMatch[1]
if (!listId) return null

return {
id: decodeURIComponent(listId),
type: "list",
view: url.searchParams.get("view"),
}
}

return null
}

const extractParamsFromAddSearchParams = (searchParams: URLSearchParams): DeepLinkParams | null => {
if (!searchParams.has("id") && !searchParams.has("url")) return null

return {
id: searchParams.get("id"),
type: searchParams.get("type"),
url: searchParams.get("url"),
view: searchParams.get("view"),
}
}

const normalizePathname = (pathname: string) => {
if (pathname.length > 1 && pathname.endsWith("/")) {
return pathname.slice(0, -1)
}
return pathname
}
2 changes: 1 addition & 1 deletion apps/ssr/client/pages/(main)/share/feeds/[id]/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const meta = defineMetadata(async ({ params, apiClient, origin }) => {
{
type: "meta",
property: "apple-itunes-app",
content: `app-id=${APPLE_APP_STORE_ID}, app-argument=follow://add?id=${feedId}&type=feed`,
content: `app-id=${APPLE_APP_STORE_ID}, app-argument=folo://add?id=${feedId}&type=feed`,
},
] as const
})
Expand Down
2 changes: 1 addition & 1 deletion apps/ssr/client/pages/(main)/share/lists/[id]/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineMetadata(async ({ params, apiClient, origin }) => {
{
type: "meta",
property: "apple-itunes-app",
content: `app-id=${APPLE_APP_STORE_ID}, app-argument=follow://add?id=${listId}&type=list`,
content: `app-id=${APPLE_APP_STORE_ID}, app-argument=folo://add?id=${listId}&type=list`,
},
]
})
2 changes: 1 addition & 1 deletion apps/ssr/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<title>Folo</title>

<meta name="apple-itunes-app" content="app-id=6739802604" />
<meta name="apple-itunes-app" content="app-id=6739802604, app-argument=folo://" />
<link rel="manifest" href="/manifest.json" />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DZMBZBW3EC"></script>
Expand Down
15 changes: 15 additions & 0 deletions apps/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Fastify from "fastify"
import { nanoid } from "nanoid"
import { FetchError } from "ofetch"

import { APPLE_APP_SITE_ASSOCIATION } from "./src/lib/apple-app-site-association"
import { MetaError } from "./src/meta-handler"
import { globalRoute } from "./src/router/global"
import { ogRoute } from "./src/router/og"
Expand Down Expand Up @@ -60,6 +61,20 @@ export const createApp = async () => {
done()
})

app.get("/.well-known/apple-app-site-association", async (_req, reply) => {
return reply
.type("application/json")
.header("Cache-Control", "public, max-age=300")
.send(APPLE_APP_SITE_ASSOCIATION)
})

app.get("/apple-app-site-association", async (_req, reply) => {
return reply
.type("application/json")
.header("Cache-Control", "public, max-age=300")
.send(APPLE_APP_SITE_ASSOCIATION)
})

if (__DEV__) {
const devVite = await import("./src/lib/dev-vite")
await devVite.registerDevViteServer(app)
Expand Down
11 changes: 11 additions & 0 deletions apps/ssr/src/lib/apple-app-site-association.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const APPLE_APP_SITE_ASSOCIATION = {
applinks: {
apps: [],
details: [
{
appID: "492J8Q67PF.is.follow",
paths: ["*"],
},
],
},
} as const
13 changes: 13 additions & 0 deletions apps/ssr/worker-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import xss from "xss"
import resvgWasm from "./resvg.wasm"
// OG image rendering
import { createFollowClient } from "./src/lib/api-client"
import { APPLE_APP_SITE_ASSOCIATION } from "./src/lib/apple-app-site-association"
import { NotFoundError } from "./src/lib/not-found"
import { setFontsBucket } from "./src/lib/og/fonts.worker"
import { setWasmModule } from "./src/lib/og/resvg-wasm-shim"
Expand Down Expand Up @@ -56,6 +57,18 @@ app.get("/profile/:path{.*}", (c) => {
return c.redirect(`/share/users/${c.req.param("path")}`, 301)
})

app.get("/.well-known/apple-app-site-association", (c) => {
return c.json(APPLE_APP_SITE_ASSOCIATION, 200, {
"Cache-Control": "public, max-age=300",
})
})

app.get("/apple-app-site-association", (c) => {
return c.json(APPLE_APP_SITE_ASSOCIATION, 200, {
"Cache-Control": "public, max-age=300",
})
})

// Middleware: set up env vars and request context
app.use("*", async (c, next) => {
if (!envInitialized) {
Expand Down
8 changes: 8 additions & 0 deletions apps/ssr/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@
"pattern": "app.folo.is/*",
"zone_id": "115ea8e6a7865dbfc1cf4530d5f87f63",
},
{
"pattern": "folo.is/.well-known/apple-app-site-association*",
"zone_id": "115ea8e6a7865dbfc1cf4530d5f87f63",
},
{
"pattern": "folo.is/apple-app-site-association*",
"zone_id": "115ea8e6a7865dbfc1cf4530d5f87f63",
},
],
"r2_buckets": [
{
Expand Down
Loading