fix: initiate OAuth using Custom Tabs on Android, closes #361 (#788)

This commit is contained in:
Huang Xin
2025-04-01 23:04:17 +08:00
committed by GitHub
parent 7ccf3d0632
commit 6131180a31
18 changed files with 161 additions and 46 deletions
@@ -19,6 +19,12 @@
android:label="@string/main_activity_title" android:label="@string/main_activity_title"
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true">
<intent-filter android:label="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="readest" android:host="auth-callback" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -2,10 +2,14 @@ package com.bilingify.readest
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.content.Intent
import android.net.Uri
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowInsetsController import android.view.WindowInsetsController
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import app.tauri.plugin.JSObject
import com.readest.native_bridge.NativeBridgePlugin
class MainActivity : TauriActivity() { class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -15,6 +19,20 @@ class MainActivity : TauriActivity() {
hideSystemUI() hideSystemUI()
} }
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val uri = intent.data ?: return
if (uri.scheme == "readest" && uri.host == "auth-callback") {
val result = JSObject().apply {
put("redirectUrl", uri.toString())
}
NativeBridgePlugin.pendingInvoke?.resolve(result)
NativeBridgePlugin.pendingInvoke = null
}
}
private fun hideSystemUI() { private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.decorView.windowInsetsController?.let { controller -> window.decorView.windowInsetsController?.let { controller ->
@@ -33,9 +33,9 @@ android {
} }
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0") implementation("androidx.appcompat:appcompat:1.6.0")
implementation("androidx.browser:browser:1.8.0")
implementation("com.google.android.material:material:1.7.0") implementation("com.google.android.material:material:1.7.0")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
@@ -3,7 +3,4 @@ package com.readest.native_bridge
import android.util.Log import android.util.Log
class NativeBridge { class NativeBridge {
fun auth_with_safari(value: String): String {
return ""
}
} }
@@ -1,7 +1,10 @@
package com.readest.native_bridge package com.readest.native_bridge
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent
import app.tauri.annotation.Command import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin import app.tauri.annotation.TauriPlugin
@@ -11,7 +14,7 @@ import app.tauri.plugin.Invoke
import java.io.* import java.io.*
@InvokeArg @InvokeArg
class SafariAuthRequestArgs { class AuthRequestArgs {
var authUrl: String? = null var authUrl: String? = null
} }
@@ -24,14 +27,25 @@ class CopyURIRequestArgs {
@TauriPlugin @TauriPlugin
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = NativeBridge() private val implementation = NativeBridge()
private var redirectScheme = "readest"
private var redirectHost = "auth-callback"
companion object {
var pendingInvoke: Invoke? = null
}
@Command @Command
fun auth_with_safari(invoke: Invoke) { fun auth_with_custom_tab(invoke: Invoke) {
val args = invoke.parseArgs(SafariAuthRequestArgs::class.java) val args = invoke.parseArgs(AuthRequestArgs::class.java)
val uri = Uri.parse(args.authUrl)
val ret = JSObject() val customTabsIntent = CustomTabsIntent.Builder().build()
ret.put("redirectUrl", implementation.auth_with_safari(args.authUrl ?: "")) customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
invoke.resolve(ret)
Log.d("NativeBridgePlugin", "Launching OAuth URL: ${args.authUrl}")
customTabsIntent.launchUrl(activity, uri)
pendingInvoke = invoke
} }
@Command @Command
@@ -1,4 +1,8 @@
const COMMANDS: &[&str] = &["auth_with_safari", "copy_uri_to_path"]; const COMMANDS: &[&str] = &[
"auth_with_safari",
"auth_with_custom_tab",
"copy_uri_to_path",
];
fn main() { fn main() {
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(COMMANDS)
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-auth-with-custom-tab"
description = "Enables the auth_with_custom_tab command without any pre-configured scope."
commands.allow = ["auth_with_custom_tab"]
[[permission]]
identifier = "deny-auth-with-custom-tab"
description = "Denies the auth_with_custom_tab command without any pre-configured scope."
commands.deny = ["auth_with_custom_tab"]
@@ -3,6 +3,7 @@
Default permissions for the plugin Default permissions for the plugin
- `allow-auth-with-safari` - `allow-auth-with-safari`
- `allow-auth-with-custom-tab`
- `allow-copy-uri-to-path` - `allow-copy-uri-to-path`
## Permission Table ## Permission Table
@@ -14,6 +15,32 @@ Default permissions for the plugin
</tr> </tr>
<tr>
<td>
`native-bridge:allow-auth-with-custom-tab`
</td>
<td>
Enables the auth_with_custom_tab command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-auth-with-custom-tab`
</td>
<td>
Denies the auth_with_custom_tab command without any pre-configured scope.
</td>
</tr>
<tr> <tr>
<td> <td>
@@ -1,3 +1,3 @@
[default] [default]
description = "Default permissions for the plugin" description = "Default permissions for the plugin"
permissions = ["allow-auth-with-safari", "allow-copy-uri-to-path"] permissions = ["allow-auth-with-safari", "allow-auth-with-custom-tab", "allow-copy-uri-to-path"]
@@ -294,6 +294,16 @@
"PermissionKind": { "PermissionKind": {
"type": "string", "type": "string",
"oneOf": [ "oneOf": [
{
"description": "Enables the auth_with_custom_tab command without any pre-configured scope.",
"type": "string",
"const": "allow-auth-with-custom-tab"
},
{
"description": "Denies the auth_with_custom_tab command without any pre-configured scope.",
"type": "string",
"const": "deny-auth-with-custom-tab"
},
{ {
"description": "Enables the auth_with_safari command without any pre-configured scope.", "description": "Enables the auth_with_safari command without any pre-configured scope.",
"type": "string", "type": "string",
@@ -7,11 +7,19 @@ use crate::Result;
#[command] #[command]
pub(crate) async fn auth_with_safari<R: Runtime>( pub(crate) async fn auth_with_safari<R: Runtime>(
app: AppHandle<R>, app: AppHandle<R>,
payload: SafariAuthRequest, payload: AuthRequest,
) -> Result<SafariAuthResponse> { ) -> Result<AuthResponse> {
app.native_bridge().auth_with_safari(payload) app.native_bridge().auth_with_safari(payload)
} }
#[command]
pub(crate) async fn auth_with_custom_tab<R: Runtime>(
app: AppHandle<R>,
payload: AuthRequest,
) -> Result<AuthResponse> {
app.native_bridge().auth_with_custom_tab(payload)
}
#[command] #[command]
pub(crate) async fn copy_uri_to_path<R: Runtime>( pub(crate) async fn copy_uri_to_path<R: Runtime>(
app: AppHandle<R>, app: AppHandle<R>,
@@ -14,10 +14,11 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
pub struct NativeBridge<R: Runtime>(AppHandle<R>); pub struct NativeBridge<R: Runtime>(AppHandle<R>);
impl<R: Runtime> NativeBridge<R> { impl<R: Runtime> NativeBridge<R> {
pub fn auth_with_safari( pub fn auth_with_safari(&self, _payload: AuthRequest) -> crate::Result<AuthResponse> {
&self, Err(crate::Error::UnsupportedPlatformError)
_payload: SafariAuthRequest, }
) -> crate::Result<SafariAuthResponse> {
pub fn auth_with_custom_tab(&self, _payload: AuthRequest) -> crate::Result<AuthResponse> {
Err(crate::Error::UnsupportedPlatformError) Err(crate::Error::UnsupportedPlatformError)
} }
@@ -38,6 +38,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("native-bridge") Builder::new("native-bridge")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::auth_with_safari, commands::auth_with_safari,
commands::auth_with_custom_tab,
commands::copy_uri_to_path, commands::copy_uri_to_path,
]) ])
.setup(|app, api| { .setup(|app, api| {
@@ -25,16 +25,21 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
pub struct NativeBridge<R: Runtime>(PluginHandle<R>); pub struct NativeBridge<R: Runtime>(PluginHandle<R>);
impl<R: Runtime> NativeBridge<R> { impl<R: Runtime> NativeBridge<R> {
pub fn auth_with_safari( pub fn auth_with_safari(&self, payload: AuthRequest) -> crate::Result<AuthResponse> {
&self,
payload: SafariAuthRequest,
) -> crate::Result<SafariAuthResponse> {
self.0 self.0
.run_mobile_plugin("auth_with_safari", payload) .run_mobile_plugin("auth_with_safari", payload)
.map_err(Into::into) .map_err(Into::into)
} }
} }
impl<R: Runtime> NativeBridge<R> {
pub fn auth_with_custom_tab(&self, payload: AuthRequest) -> crate::Result<AuthResponse> {
self.0
.run_mobile_plugin("auth_with_custom_tab", payload)
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> { impl<R: Runtime> NativeBridge<R> {
pub fn copy_uri_to_path(&self, payload: CopyURIRequest) -> crate::Result<CopyURIResponse> { pub fn copy_uri_to_path(&self, payload: CopyURIRequest) -> crate::Result<CopyURIResponse> {
self.0 self.0
@@ -2,13 +2,13 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SafariAuthRequest { pub struct AuthRequest {
pub auth_url: String, pub auth_url: String,
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SafariAuthResponse { pub struct AuthResponse {
pub redirect_url: String, pub redirect_url: String,
} }
+8 -5
View File
@@ -23,7 +23,7 @@ import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oau
import { openUrl } from '@tauri-apps/plugin-opener'; import { openUrl } from '@tauri-apps/plugin-opener';
import { handleAuthCallback } from '@/helpers/auth'; import { handleAuthCallback } from '@/helpers/auth';
import { getAppleIdAuth, Scope } from './utils/appleIdAuth'; import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
import { authWithSafari } from './utils/safariAuth'; import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
import { READEST_WEB_BASE_URL } from '@/services/constants'; import { READEST_WEB_BASE_URL } from '@/services/constants';
import WindowButtons from '@/components/WindowButtons'; import WindowButtons from '@/components/WindowButtons';
@@ -42,7 +42,7 @@ interface ProviderLoginProp {
} }
const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`; const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`;
const DEEPLINK_CALLBACK = 'readest://auth/callback'; const DEEPLINK_CALLBACK = 'readest://auth-callback';
const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => { const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => {
return ( return (
@@ -75,10 +75,8 @@ export default function AuthPage() {
const getTauriRedirectTo = (isOAuth: boolean) => { const getTauriRedirectTo = (isOAuth: boolean) => {
if (process.env.NODE_ENV === 'production' || appService?.isMobile) { if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
if (appService?.isIOSApp) { if (appService?.isMobile) {
return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK; return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK;
} else if (appService?.isAndroidApp) {
return WEB_AUTH_CALLBACK;
} }
return DEEPLINK_CALLBACK; return DEEPLINK_CALLBACK;
} }
@@ -135,6 +133,11 @@ export default function AuthPage() {
if (res) { if (res) {
handleOAuthUrl(res.redirectUrl); handleOAuthUrl(res.redirectUrl);
} }
} else if (appService?.isAndroidApp) {
const res = await authWithCustomTab({ authUrl: data.url });
if (res) {
handleOAuthUrl(res.redirectUrl);
}
} else { } else {
await openUrl(data.url); await openUrl(data.url);
} }
@@ -0,0 +1,25 @@
import { invoke } from '@tauri-apps/api/core';
export interface AuthRequest {
authUrl: string;
}
export interface AuthResponse {
redirectUrl: string;
}
export async function authWithSafari(request: AuthRequest): Promise<AuthResponse> {
const result = await invoke<AuthResponse>('plugin:native-bridge|auth_with_safari', {
payload: request,
});
return result;
}
export async function authWithCustomTab(request: AuthRequest): Promise<AuthResponse> {
const result = await invoke<AuthResponse>('plugin:native-bridge|auth_with_custom_tab', {
payload: request,
});
return result;
}
@@ -1,17 +0,0 @@
import { invoke } from '@tauri-apps/api/core';
export interface SafariAuthRequest {
authUrl: string;
}
export interface SafariAuthResponse {
redirectUrl: string;
}
export async function authWithSafari(request: SafariAuthRequest): Promise<SafariAuthResponse> {
const result = await invoke<SafariAuthResponse>('plugin:native-bridge|auth_with_safari', {
payload: request,
});
return result;
}