feat(sync): Google Drive sign-in on Android + iOS (mobile OAuth) (#4823)
* feat(sync): Google Drive sign-in on Android (Custom Tab OAuth) Add the Android OAuth runner so Drive can be connected on Android, reusing the same provider / token store / connect flow as desktop. - oauthAndroid.ts: runAndroidOAuth wires the DI OAuth flow to a Chrome Custom Tab via the existing authWithCustomTab native bridge (keeps the Tauri Activity foregrounded so the in-flight redirect survives). Headless-unit-tested. - googleDriveConnect: dispatch the platform runner by OS (Android -> Custom Tab, desktop -> system browser deep link). - IntegrationsPanel: show the Google Drive provider row on Android too. - Native (device-verification pending — no Android toolchain in CI): NativeBridgePlugin.kt handleIntent now also resolves the reverse-DNS com.googleusercontent.apps.<id>:/oauthredirect redirect through the same pending invoke as the Supabase callback; a matching BROWSABLE intent-filter added to AndroidManifest.xml (mirrors the tauri.conf.json deep-link scheme). Full suite 6475 green; lint + format clean. The native sign-in needs on-device Android verification before this ships. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): Google Drive sign-in on iOS (ASWebAuthenticationSession OAuth) Add the iOS OAuth runner so the Drive provider connects on iPhone/iPad, mirroring the Android Custom Tab flow. - oauthIos.ts: runIosOAuth drives the shared PKCE flow through authWithSafari, keyed to the client-id-derived reverse-DNS callback scheme so the web-auth session intercepts the redirect. - nativeAuth.ts: AuthRequest gains an optional callbackScheme; the Supabase login keeps the native "readest" default. - googleDriveConnect.ts: resolveOAuthRunner dispatches ios to runIosOAuth. - IntegrationsPanel.tsx: show the Google Drive cloud-sync row on iOS. Native (device-verify pending, no iOS toolchain in CI): - auth_with_safari honors args.callbackScheme (default "readest"). - Info-ios.plist registers the reverse-DNS scheme in CFBundleURLTypes, mirroring the AndroidManifest gdrive-oauth filter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,18 @@
|
||||
<string>readest</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- Google Drive OAuth reverse-DNS redirect (iOS-type client). The
|
||||
ASWebAuthenticationSession matches on this scheme; mirrors the
|
||||
gdrive-oauth filter in AndroidManifest.xml and the deep-link.mobile
|
||||
scheme in tauri.conf.json. -->
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.googleusercontent.apps.gdrive-oauth</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.209390247301-ctpmep68ppfa56r1b8tr35e4qi4p60kq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -95,6 +95,16 @@
|
||||
<data android:scheme="readest" android:host="auth-callback" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Google Drive OAuth reverse-DNS redirect (iOS-type client). Mirrors
|
||||
the deep-link.mobile scheme in tauri.conf.json so the redirect
|
||||
routes back even if the auto-generated section is regenerated. -->
|
||||
<intent-filter android:label="gdrive-oauth">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="com.googleusercontent.apps.209390247301-ctpmep68ppfa56r1b8tr35e4qi4p60kq" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
+8
-1
@@ -189,7 +189,14 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
// OAuth callback uses a custom scheme on intent.data and is handled
|
||||
// separately from any user-shared content.
|
||||
intent.data?.let { uri ->
|
||||
if (uri.scheme == "readest" && uri.host == "auth-callback") {
|
||||
val scheme = uri.scheme ?: ""
|
||||
val isReadestAuth = scheme == "readest" && uri.host == "auth-callback"
|
||||
// Google Drive sign-in uses the reverse-DNS "iOS URL scheme"
|
||||
// (com.googleusercontent.apps.<id>:/oauthredirect) registered as a
|
||||
// BROWSABLE deep link; resolve it through the same pending invoke as
|
||||
// the Supabase readest://auth-callback flow.
|
||||
val isGoogleOAuth = scheme.startsWith("com.googleusercontent.apps.")
|
||||
if (isReadestAuth || isGoogleOAuth) {
|
||||
val result = JSObject().apply {
|
||||
put("redirectUrl", uri.toString())
|
||||
}
|
||||
|
||||
+6
-1
@@ -30,6 +30,10 @@ func getLocalizedDisplayName(familyName: String) -> String? {
|
||||
|
||||
class SafariAuthRequestArgs: Decodable {
|
||||
let authUrl: String
|
||||
// ASWebAuthenticationSession callback scheme. Defaults to "readest" (the
|
||||
// Supabase login); the Google Drive flow passes its reverse-DNS scheme so the
|
||||
// session intercepts that redirect instead.
|
||||
let callbackScheme: String?
|
||||
}
|
||||
|
||||
class UseBackgroundAudioRequestArgs: Decodable {
|
||||
@@ -770,8 +774,9 @@ class NativeBridgePlugin: Plugin {
|
||||
@objc public func auth_with_safari(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(SafariAuthRequestArgs.self)
|
||||
let authUrl = URL(string: args.authUrl)!
|
||||
let callbackScheme = args.callbackScheme ?? "readest"
|
||||
|
||||
authSession = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: "readest") {
|
||||
authSession = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) {
|
||||
[weak self] callbackURL, error in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/app/auth/utils/nativeAuth', () => ({
|
||||
authWithCustomTab: vi.fn(async ({ authUrl }: { authUrl: string }) => {
|
||||
// The native Custom Tab echoes the redirect, with the exact `state` the
|
||||
// flow put on the consent URL.
|
||||
const state = new URL(authUrl).searchParams.get('state');
|
||||
return {
|
||||
redirectUrl: `com.googleusercontent.apps.cid:/oauthredirect?code=CODE&state=${state}`,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { runAndroidOAuth } from '@/services/sync/providers/gdrive/auth/oauthAndroid';
|
||||
import { authWithCustomTab } from '@/app/auth/utils/nativeAuth';
|
||||
import type { FetchFn } from '@/services/sync/providers/gdrive/auth/tokenStore';
|
||||
|
||||
const CLIENT_ID = 'cid.apps.googleusercontent.com';
|
||||
|
||||
const tokenJson = (): Response =>
|
||||
new Response(JSON.stringify({ access_token: 'AT', refresh_token: 'RT', expires_in: 3600 }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
describe('runAndroidOAuth', () => {
|
||||
test('opens a Custom Tab, captures the redirect, and exchanges the code', async () => {
|
||||
const fetchFn = vi.fn(async () => tokenJson()) as unknown as FetchFn;
|
||||
const tokens = await runAndroidOAuth({ clientId: CLIENT_ID, scope: 'drive.file' }, fetchFn);
|
||||
|
||||
expect(authWithCustomTab).toHaveBeenCalledTimes(1);
|
||||
// The Custom Tab is handed the consent URL (with the PKCE challenge + state).
|
||||
const authUrl = vi.mocked(authWithCustomTab).mock.calls[0]![0].authUrl;
|
||||
expect(authUrl).toContain('code_challenge=');
|
||||
expect(authUrl).toContain('accounts.google.com');
|
||||
expect(tokens.accessToken).toBe('AT');
|
||||
expect(tokens.refreshToken).toBe('RT');
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('propagates a CSRF state mismatch from the redirect', async () => {
|
||||
vi.mocked(authWithCustomTab).mockResolvedValueOnce({
|
||||
redirectUrl: 'com.googleusercontent.apps.cid:/oauthredirect?code=CODE&state=ATTACKER',
|
||||
});
|
||||
const fetchFn = vi.fn(async () => tokenJson()) as unknown as FetchFn;
|
||||
await expect(
|
||||
runAndroidOAuth({ clientId: CLIENT_ID, scope: 'drive.file' }, fetchFn),
|
||||
).rejects.toThrow(/state mismatch/i);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/app/auth/utils/nativeAuth', () => ({
|
||||
authWithSafari: vi.fn(async ({ authUrl }: { authUrl: string; callbackScheme?: string }) => {
|
||||
// The native web-auth session echoes the redirect, with the exact `state`
|
||||
// the flow put on the consent URL.
|
||||
const state = new URL(authUrl).searchParams.get('state');
|
||||
return {
|
||||
redirectUrl: `com.googleusercontent.apps.cid:/oauthredirect?code=CODE&state=${state}`,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
import { runIosOAuth } from '@/services/sync/providers/gdrive/auth/oauthIos';
|
||||
import { authWithSafari } from '@/app/auth/utils/nativeAuth';
|
||||
import type { FetchFn } from '@/services/sync/providers/gdrive/auth/tokenStore';
|
||||
|
||||
const CLIENT_ID = 'cid.apps.googleusercontent.com';
|
||||
|
||||
const tokenJson = (): Response =>
|
||||
new Response(JSON.stringify({ access_token: 'AT', refresh_token: 'RT', expires_in: 3600 }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
||||
describe('runIosOAuth', () => {
|
||||
test('opens a web-auth session keyed to the reverse-DNS scheme and exchanges the code', async () => {
|
||||
const fetchFn = vi.fn(async () => tokenJson()) as unknown as FetchFn;
|
||||
const tokens = await runIosOAuth({ clientId: CLIENT_ID, scope: 'drive.file' }, fetchFn);
|
||||
|
||||
expect(authWithSafari).toHaveBeenCalledTimes(1);
|
||||
const arg = vi.mocked(authWithSafari).mock.calls[0]![0];
|
||||
// The session is handed the consent URL (with the PKCE challenge + state)...
|
||||
expect(arg.authUrl).toContain('code_challenge=');
|
||||
expect(arg.authUrl).toContain('accounts.google.com');
|
||||
// ...and the bare reverse-DNS callback scheme (no path) it must intercept.
|
||||
expect(arg.callbackScheme).toBe('com.googleusercontent.apps.cid');
|
||||
expect(tokens.accessToken).toBe('AT');
|
||||
expect(tokens.refreshToken).toBe('RT');
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('propagates a CSRF state mismatch from the redirect', async () => {
|
||||
vi.mocked(authWithSafari).mockResolvedValueOnce({
|
||||
redirectUrl: 'com.googleusercontent.apps.cid:/oauthredirect?code=CODE&state=ATTACKER',
|
||||
});
|
||||
const fetchFn = vi.fn(async () => tokenJson()) as unknown as FetchFn;
|
||||
await expect(
|
||||
runIosOAuth({ clientId: CLIENT_ID, scope: 'drive.file' }, fetchFn),
|
||||
).rejects.toThrow(/state mismatch/i);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,12 @@ import { type as osType } from '@tauri-apps/plugin-os';
|
||||
|
||||
export interface AuthRequest {
|
||||
authUrl: string;
|
||||
/**
|
||||
* iOS `ASWebAuthenticationSession` callback scheme. Defaults to `readest`
|
||||
* natively (the Supabase login); the Google Drive flow passes its reverse-DNS
|
||||
* scheme so the session intercepts that redirect instead.
|
||||
*/
|
||||
callbackScheme?: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
|
||||
@@ -290,7 +290,7 @@ const IntegrationsPanel: React.FC = () => {
|
||||
onOpen={() => setSubPage('webdav')}
|
||||
activateLabel={_('Use WebDAV')}
|
||||
/>
|
||||
{appService?.isDesktopApp && (
|
||||
{(appService?.isDesktopApp || appService?.isAndroidApp || appService?.isIOSApp) && (
|
||||
<CloudProviderRow
|
||||
icon={RiGoogleLine}
|
||||
title={_('Google Drive')}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Android wiring of the OAuth authorization-code + PKCE flow: a Chrome Custom Tab
|
||||
* + reverse-DNS custom-scheme redirect.
|
||||
*
|
||||
* Android can't use a loopback redirect (the system browser can't hand a
|
||||
* `http://127.0.0.1` back to the app). Instead we use the reverse-DNS scheme an
|
||||
* iOS-type Google client issues — `com.googleusercontent.apps.<id>:/oauthredirect`
|
||||
* — registered as a BROWSABLE deep-link intent-filter so the OS routes the
|
||||
* redirect back to the app.
|
||||
*
|
||||
* Consent opens in a CHROME CUSTOM TAB via Readest's native bridge command
|
||||
* `auth_with_custom_tab` (the same one the Supabase login uses), NOT an external
|
||||
* browser: a separate browser app backgrounds the single Tauri Activity and the
|
||||
* OS can destroy it under memory pressure, tearing down the in-flight promise +
|
||||
* redirect listener (observed on-device: first attempt fails, second succeeds). A
|
||||
* Custom Tab renders inside the host task and keeps the process at foreground
|
||||
* importance; the redirect resolves through a native field that survives a WebView
|
||||
* reload. The native command opens consent AND resolves with the redirect URL in
|
||||
* one round trip, recognising it by the reverse-DNS scheme parsed from the auth
|
||||
* URL's `redirect_uri`.
|
||||
*
|
||||
* The iOS-type client has NO secret and needs NO Android SHA-1 — Google validates
|
||||
* the redirect by string-matching the client-id-derived scheme, and PKCE is the
|
||||
* real client authentication. This holds only while App Check (iOS attestation)
|
||||
* is off — an Android device can't produce an iOS attestation, so enabling it
|
||||
* would break every user; keep it off.
|
||||
*
|
||||
* Adapted from ratatabananana-bit/Readest-google-drive-mod-patcher (AGPL-3.0),
|
||||
* used with the author's explicit permission.
|
||||
*/
|
||||
import { authWithCustomTab } from '@/app/auth/utils/nativeAuth';
|
||||
import { createPkcePair } from './pkce';
|
||||
import { deriveReverseDnsRedirectUri } from './reverseDnsRedirect';
|
||||
import { runOAuthFlow, type OAuthClientConfig } from './oauthFlow';
|
||||
import { exchangeCode, type FetchFn, type TokenSet } from './tokenStore';
|
||||
|
||||
/**
|
||||
* Run the Android Custom-Tab OAuth flow and return the resulting tokens. Wires
|
||||
* {@link runOAuthFlow} with the Android mechanics: open consent in a Chrome
|
||||
* Custom Tab and resolve with the redirect the native bridge captures.
|
||||
*/
|
||||
export const runAndroidOAuth = (config: OAuthClientConfig, fetchFn: FetchFn): Promise<TokenSet> => {
|
||||
const redirectUri = deriveReverseDnsRedirectUri(config.clientId);
|
||||
|
||||
// `auth_with_custom_tab` opens consent AND resolves with the redirect URL in a
|
||||
// single native round-trip — so it needs the auth URL, which `runOAuthFlow`
|
||||
// only hands to `openUrl`. Bridge the two with a deferred: `openUrl` supplies
|
||||
// the URL, and `awaitRedirect` (invoked first) waits for it before driving the
|
||||
// Custom Tab. The native side recognises the redirect by its reverse-DNS
|
||||
// scheme, so only the auth URL needs to cross over.
|
||||
let provideAuthUrl!: (url: string) => void;
|
||||
const authUrlReady = new Promise<string>((resolve) => {
|
||||
provideAuthUrl = resolve;
|
||||
});
|
||||
|
||||
return runOAuthFlow(config.scope, {
|
||||
createPkcePair,
|
||||
newState: () => crypto.randomUUID(),
|
||||
clientId: config.clientId,
|
||||
openUrl: async (url) => {
|
||||
provideAuthUrl(url);
|
||||
},
|
||||
awaitRedirect: async () => {
|
||||
const authUrl = await authUrlReady;
|
||||
const { redirectUrl } = await authWithCustomTab({ authUrl });
|
||||
return redirectUrl;
|
||||
},
|
||||
redirectUri,
|
||||
exchange: ({ code, verifier, redirectUri: uri }) =>
|
||||
exchangeCode({ code, verifier, clientId: config.clientId, redirectUri: uri }, fetchFn),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* iOS wiring of the OAuth authorization-code + PKCE flow: an
|
||||
* `ASWebAuthenticationSession` + reverse-DNS custom-scheme redirect.
|
||||
*
|
||||
* Like Android, iOS can't use a loopback redirect, so we use the reverse-DNS
|
||||
* scheme the iOS-type Google client issues —
|
||||
* `com.googleusercontent.apps.<id>:/oauthredirect` — registered in
|
||||
* `Info-ios.plist` `CFBundleURLTypes` so the OS routes the redirect back.
|
||||
*
|
||||
* Consent opens in an `ASWebAuthenticationSession` via Readest's native bridge
|
||||
* command `auth_with_safari` (the same one the Supabase login uses). That session
|
||||
* intercepts the redirect by its `callbackURLScheme` — so unlike the desktop
|
||||
* deep-link runner, no app-wide URL listener is needed: the native command opens
|
||||
* consent AND resolves with the redirect URL in one round trip. The callback
|
||||
* scheme is the bare reverse-DNS scheme derived from the client id (no path),
|
||||
* which is exactly what `ASWebAuthenticationSession` matches on.
|
||||
*
|
||||
* The iOS-type client has NO secret — Google validates the redirect by
|
||||
* string-matching the client-id-derived scheme, and PKCE is the real client
|
||||
* authentication. This holds only while App Check (iOS attestation) is off.
|
||||
*
|
||||
* Adapted from ratatabananana-bit/Readest-google-drive-mod-patcher (AGPL-3.0),
|
||||
* used with the author's explicit permission.
|
||||
*/
|
||||
import { authWithSafari } from '@/app/auth/utils/nativeAuth';
|
||||
import { createPkcePair } from './pkce';
|
||||
import { deriveReverseDnsRedirectScheme, deriveReverseDnsRedirectUri } from './reverseDnsRedirect';
|
||||
import { runOAuthFlow, type OAuthClientConfig } from './oauthFlow';
|
||||
import { exchangeCode, type FetchFn, type TokenSet } from './tokenStore';
|
||||
|
||||
/**
|
||||
* Run the iOS `ASWebAuthenticationSession` OAuth flow and return the resulting
|
||||
* tokens. Wires {@link runOAuthFlow} with the iOS mechanics: open consent in a
|
||||
* web-auth session keyed to the reverse-DNS callback scheme and resolve with the
|
||||
* redirect the native bridge captures.
|
||||
*/
|
||||
export const runIosOAuth = (config: OAuthClientConfig, fetchFn: FetchFn): Promise<TokenSet> => {
|
||||
const redirectUri = deriveReverseDnsRedirectUri(config.clientId);
|
||||
// ASWebAuthenticationSession matches on the bare scheme (no path); pass the
|
||||
// client-id-derived reverse-DNS scheme so the native session recognises the
|
||||
// `com.googleusercontent.apps.<id>:/oauthredirect?...` bounce-back.
|
||||
const callbackScheme = deriveReverseDnsRedirectScheme(config.clientId);
|
||||
|
||||
// `auth_with_safari` opens consent AND resolves with the redirect URL in a
|
||||
// single native round-trip — so it needs the auth URL, which `runOAuthFlow`
|
||||
// only hands to `openUrl`. Bridge the two with a deferred: `openUrl` supplies
|
||||
// the URL, and `awaitRedirect` (invoked first) waits for it before driving the
|
||||
// web-auth session.
|
||||
let provideAuthUrl!: (url: string) => void;
|
||||
const authUrlReady = new Promise<string>((resolve) => {
|
||||
provideAuthUrl = resolve;
|
||||
});
|
||||
|
||||
return runOAuthFlow(config.scope, {
|
||||
createPkcePair,
|
||||
newState: () => crypto.randomUUID(),
|
||||
clientId: config.clientId,
|
||||
openUrl: async (url) => {
|
||||
provideAuthUrl(url);
|
||||
},
|
||||
awaitRedirect: async () => {
|
||||
const authUrl = await authUrlReady;
|
||||
const { redirectUrl } = await authWithSafari({ authUrl, callbackScheme });
|
||||
return redirectUrl;
|
||||
},
|
||||
redirectUri,
|
||||
exchange: ({ code, verifier, redirectUri: uri }) =>
|
||||
exchangeCode({ code, verifier, clientId: config.clientId, redirectUri: uri }, fetchFn),
|
||||
});
|
||||
};
|
||||
@@ -1,14 +1,19 @@
|
||||
/**
|
||||
* Wire the platform OAuth runner + env client id + keychain into the
|
||||
* {@link connectGoogleDrive} orchestration, so the settings UI's Connect button
|
||||
* is a single call. Desktop only for now — Android / iOS runners land in later
|
||||
* phases; the Drive row is hidden off-desktop.
|
||||
* is a single call. The runner is platform-resolved (desktop deep-link, Android
|
||||
* Custom Tab, iOS web-auth session); see {@link resolveOAuthRunner}.
|
||||
*/
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { getGoogleClientId } from './buildGoogleDriveProvider';
|
||||
import { createDriveTokenPersistence } from './driveTokenStore';
|
||||
import { runDesktopDeepLinkOAuth } from './auth/oauthDesktop';
|
||||
import { runAndroidOAuth } from './auth/oauthAndroid';
|
||||
import { runIosOAuth } from './auth/oauthIos';
|
||||
import type { OAuthClientConfig } from './auth/oauthFlow';
|
||||
import type { TokenSet } from './auth/tokenStore';
|
||||
import {
|
||||
connectGoogleDrive,
|
||||
disconnectGoogleDrive,
|
||||
@@ -19,7 +24,26 @@ import type { FetchFn } from './GoogleDriveProvider';
|
||||
const resolveFetch = (): FetchFn =>
|
||||
(isTauriAppPlatform() ? tauriFetch : globalThis.fetch) as unknown as FetchFn;
|
||||
|
||||
/** Run the desktop Drive sign-in and return the connected account label. */
|
||||
/**
|
||||
* Pick the platform OAuth runner: Android uses a Chrome Custom Tab; iOS uses an
|
||||
* `ASWebAuthenticationSession`; desktop (macOS/Windows/Linux) uses the system
|
||||
* browser + deep-link.
|
||||
*/
|
||||
const resolveOAuthRunner = (): ((
|
||||
config: OAuthClientConfig,
|
||||
fetchFn: FetchFn,
|
||||
) => Promise<TokenSet>) => {
|
||||
try {
|
||||
const os = osType();
|
||||
if (os === 'android') return runAndroidOAuth;
|
||||
if (os === 'ios') return runIosOAuth;
|
||||
} catch {
|
||||
// osType() is Tauri-only; off-Tauri this path isn't reached anyway.
|
||||
}
|
||||
return runDesktopDeepLinkOAuth;
|
||||
};
|
||||
|
||||
/** Run the platform Drive sign-in and return the connected account label. */
|
||||
export const runGoogleDriveConnect = async (): Promise<ConnectGoogleDriveResult> => {
|
||||
const clientId = getGoogleClientId();
|
||||
if (!clientId) {
|
||||
@@ -27,13 +51,13 @@ export const runGoogleDriveConnect = async (): Promise<ConnectGoogleDriveResult>
|
||||
}
|
||||
const persistence = await createDriveTokenPersistence();
|
||||
if (!persistence) {
|
||||
throw new Error('Google Drive requires the desktop app with secure storage');
|
||||
throw new Error('Google Drive requires a Readest app build with secure storage');
|
||||
}
|
||||
return connectGoogleDrive({
|
||||
clientId,
|
||||
fetchFn: resolveFetch(),
|
||||
persistence,
|
||||
runOAuth: runDesktopDeepLinkOAuth,
|
||||
runOAuth: resolveOAuthRunner(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user