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
19 changes: 19 additions & 0 deletions apps/desktop/changelog/1.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# What's new in v1.4.0

## Shiny new things

- Added in-app review prompts

## Improvements

- Expanded desktop end-to-end coverage for auth and user flows

## No longer broken

- Removed the unwanted text selection toolbar
- Fixed AI onboarding asset loading by switching the spline asset domain
- Hardened setting sync authentication lifecycle

## Thanks

Special thanks to volunteer contributors for their valuable contributions
44 changes: 0 additions & 44 deletions apps/desktop/layer/main/src/ipc/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { IpcContext } from "electron-ipc-decorator"
import { IpcMethod, IpcService } from "electron-ipc-decorator"

import { BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN } from "~/constants/app"
import { apiClient } from "~/lib/api-client"
import { WindowManager } from "~/manager/window"

import { getSessionTokenFromCookies, syncSessionToCliConfig } from "../../lib/cli-session-sync"
Expand Down Expand Up @@ -135,49 +134,6 @@ export class AuthService extends IpcService {
await this.clearSessionToken()
}

@IpcMethod()
async getSession(_context: IpcContext) {
return apiClient.auth.getSession()
}

@IpcMethod()
async getSessionByToken(_context: IpcContext, token: string) {
const response = await fetch(`${env.VITE_API_URL}/better-auth/get-session`, {
headers: {
...createDesktopAPIHeaders({ version: PKG.version }),
Cookie: `__Secure-better-auth.session_token=${token}; better-auth.session_token=${token}`,
},
})

return response.json().catch(async () => ({ message: await response.text() }))
}

@IpcMethod()
async request(
_context: IpcContext,
payload: {
input: string
init?: {
method?: string
headers?: Record<string, string>
body?: string
}
},
) {
const response = await fetch(payload.input, {
method: payload.init?.method,
headers: payload.init?.headers,
body: payload.init?.body,
cache: "no-store",
})

return {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
body: await response.text(),
}
}

@IpcMethod()
async signInWithCredential(
_context: IpcContext,
Expand Down
56 changes: 5 additions & 51 deletions apps/desktop/layer/renderer/src/lib/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IN_ELECTRON } from "@follow/shared/constants"
import { env } from "@follow/shared/env.desktop"
import { whoami } from "@follow/store/user/getters"
import { userActions } from "@follow/store/user/store"
Expand All @@ -9,53 +8,17 @@ import PKG from "@pkg"
import { NetworkStatus, setApiStatus } from "~/atoms/network"
import { setLoginModalShow } from "~/atoms/user"

import { ipcServices } from "./client"
import { getAuthSessionToken, getClientId, getSessionId } from "./client-session"

const electronFetch = async (input: string | URL | Request, options: RequestInit = {}) => {
const authService = ipcServices?.auth as
| (NonNullable<typeof ipcServices>["auth"] & {
request?: (payload: {
input: string
init?: { method?: string; headers?: Record<string, string>; body?: string }
}) => Promise<{ status: number; headers: Record<string, string>; body: string }>
})
| undefined

if (!authService?.request) {
return fetch(input.toString(), {
...options,
cache: "no-store",
})
}

const headers = new Headers(options.headers)
const response = await authService.request({
input: input.toString(),
init: {
method: options.method,
headers: Object.fromEntries(headers.entries()),
body: typeof options.body === "string" ? options.body : undefined,
},
})

return new Response(response.body, {
status: response.status,
headers: response.headers,
})
}
import { getClientId, getSessionId } from "./client-session"

export const followClient = new FollowClient({
credentials: "include",
timeout: 30000,
baseURL: env.VITE_API_URL,
fetch: async (input, options = {}) =>
IN_ELECTRON
? electronFetch(input, options)
: fetch(input.toString(), {
...options,
cache: "no-store",
}),
fetch(input.toString(), {
...options,
cache: "no-store",
}),
})

export const followApi = followClient.api
Expand All @@ -65,11 +28,6 @@ followClient.addRequestInterceptor(async (ctx) => {
header["X-Client-Id"] = getClientId()
header["X-Session-Id"] = getSessionId()

const authSessionToken = IN_ELECTRON ? getAuthSessionToken() : null
if (authSessionToken) {
header.Cookie = `__Secure-better-auth.session_token=${authSessionToken}; better-auth.session_token=${authSessionToken}`
}

const apiHeader = createDesktopAPIHeaders({ version: PKG.version })

options.headers = {
Expand Down Expand Up @@ -107,10 +65,6 @@ followClient.addResponseInterceptor(async ({ response }) => {
return response
}

if (IN_ELECTRON && getAuthSessionToken()) {
return response
}

// Or we can present LoginModal here.
// router.navigate("/login")
// If any response status is 401, we can set auth fail. Maybe some bug, but if navigate to login page, had same issues
Expand Down
32 changes: 1 addition & 31 deletions apps/desktop/layer/renderer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import ReactDOM from "react-dom/client"
import { RouterProvider } from "react-router/dom"

import { authClient } from "~/lib/auth"
import { ipcServices } from "~/lib/client"
import { getAuthSessionToken } from "~/lib/client-session"

import { setAppIsReady } from "./atoms/app"
import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "./constants"
Expand All @@ -24,35 +22,7 @@ import { router } from "./router"

authClientContext.provide(authClient)
queryClientContext.provide(queryClient)

const providedApi = IN_ELECTRON
? {
...followApi,
auth: {
...followApi.auth,
getSession: async (...args: Parameters<typeof followApi.auth.getSession>) => {
const authService = ipcServices?.auth as
| (typeof followApi.auth & {
getSession?: () => ReturnType<typeof followApi.auth.getSession>
getSessionByToken?: (token: string) => ReturnType<typeof followApi.auth.getSession>
})
| undefined
const authSessionToken = getAuthSessionToken()
const session = authSessionToken
? await authService?.getSessionByToken?.(authSessionToken)
: await authService?.getSession?.()

if (session) {
return session
}

return followApi.auth.getSession(...args)
},
},
}
: followApi

apiContext.provide(providedApi)
apiContext.provide(followApi)

Choose a reason for hiding this comment

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

P1 Badge Restore Electron token fallback for initial session fetch

Providing followApi directly here removes the Electron-specific auth.getSession override that previously called ipcServices.auth.getSessionByToken(getAuthSessionToken()) before falling back to HTTP. The login/register flow still sets the cookie asynchronously (setElectronSessionToken(token) is fire-and-forget in modules/auth/Form.tsx) and then immediately reloads (handleSessionChanges), so the first post-login api().auth.getSession() can run before the cookie is persisted. In that window Electron now gets a 401 and treats the user as unauthenticated, which can surface as intermittent post-login failures/loops.

Useful? React with 👍 / 👎.


initializeApp().finally(() => {
import("./push-notification").then(({ registerWebPushNotifications }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { defineSettingPageData } from "~/modules/settings/utils"

const iconName = "i-mgc-terminal-cute-re"
const priority = (1000 << 1) + 25
const CLI_SETTINGS_DISABLED_FOR_THIS_RELEASE = true

export const loader = defineSettingPageData({
icon: iconName,
name: "titles.cli",
priority,
hideIf: () => !IN_ELECTRON,
hideIf: () => CLI_SETTINGS_DISABLED_FOR_THIS_RELEASE || !IN_ELECTRON,
})

export function Component() {
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Folo",
"type": "module",
"version": "1.3.1",
"version": "1.4.0",
"private": true,
"description": "Follow everything in one place",
"author": "Folo Team",
Expand Down Expand Up @@ -95,5 +95,5 @@
"vite-tsconfig-paths": "6.1.1"
},
"productName": "Folo",
"mainHash": "906a0b82e00829115764f36a38132f3774120c4bea764e3b1be8949f447ccb06"
"mainHash": "0c464fca7c98fd4b42abba743abb1cd590e216043987acf4f6d1e10392ce0e57"
}
27 changes: 27 additions & 0 deletions apps/mobile/changelog/0.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# What's New in v0.4.0

## Shiny new things

- Added Apple subscriptions on iOS
- Supported anonymous timeline reading without signing in
- Refined upgrade prompts to match desktop subscription flows
- Added in-app review prompts
- Supported French localization

## Improvements

- Polished settings and Discover UI across iOS and Android
- Improved subscription and plan management surfaces
- Hardened authentication flows and expanded end-to-end coverage
- Aligned translation gating with actual user roles

## No longer broken

- Fixed discover category loading and iOS auth bootstrap issues
- Fixed iOS subscription upgrade actions
- Fixed Android modal header overlap
- Stabilized sign-in persistence across mobile auth flows

## Thanks

Special thanks to volunteer contributors for their valuable contributions
4 changes: 2 additions & 2 deletions apps/mobile/ios/Folo/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>0.3.0</string>
<string>0.4.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand All @@ -54,7 +54,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>138</string>
<string>139</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@follow/mobile",
"version": "0.3.0",
"version": "0.4.0",
"private": true,
"main": "src/main.tsx",
"scripts": {
Expand Down
Loading