mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Compare commits
1 Commits
pysssss/ap
...
feat/disab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e898a6e72f |
@@ -49,6 +49,7 @@
|
||||
<div class="flex flex-col gap-6">
|
||||
<template v-if="ssoAllowed">
|
||||
<Button
|
||||
v-if="!isWebView"
|
||||
type="button"
|
||||
class="h-10"
|
||||
variant="secondary"
|
||||
@@ -157,6 +158,7 @@ import type { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { isHostWhitelisted, normalizeHost } from '@/utils/hostWhitelist'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { isEmbeddedWebView } from '@/utils/webviewDetection'
|
||||
|
||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||
import SignInForm from './signin/SignInForm.vue'
|
||||
@@ -172,6 +174,7 @@ const isSecureContext = window.isSecureContext
|
||||
const isSignIn = ref(true)
|
||||
const showApiKeyForm = ref(false)
|
||||
const ssoAllowed = isHostWhitelisted(normalizeHost(window.location.hostname))
|
||||
const isWebView = isEmbeddedWebView()
|
||||
const comfyPlatformBaseUrl = computed(() =>
|
||||
configValueOrDefault(
|
||||
remoteConfig.value,
|
||||
|
||||
@@ -39,7 +39,12 @@
|
||||
<template v-if="!showEmailForm">
|
||||
<!-- OAuth Buttons (primary) -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<Button type="button" class="h-10 w-full" @click="signInWithGoogle">
|
||||
<Button
|
||||
v-if="!isWebView"
|
||||
type="button"
|
||||
class="h-10 w-full"
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{ t('auth.login.loginWithGoogle') }}
|
||||
</Button>
|
||||
@@ -116,6 +121,7 @@ import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/u
|
||||
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { isEmbeddedWebView } from '@/utils/webviewDetection'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -126,6 +132,7 @@ const authError = ref('')
|
||||
const toastStore = useToastStore()
|
||||
const showEmailForm = ref(false)
|
||||
const { isFreeTierEnabled, freeTierCredits } = useFreeTierOnboarding()
|
||||
const isWebView = isEmbeddedWebView()
|
||||
|
||||
function switchToEmailForm() {
|
||||
showEmailForm.value = true
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<!-- OAuth Buttons (primary) -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="relative">
|
||||
<div v-if="!isWebView" class="relative">
|
||||
<Button type="button" class="h-10 w-full" @click="signInWithGoogle">
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{ t('auth.signup.signUpWithGoogle') }}
|
||||
@@ -141,6 +141,7 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { SignUpData } from '@/schemas/signInSchema'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { isEmbeddedWebView } from '@/utils/webviewDetection'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
@@ -158,6 +159,7 @@ const {
|
||||
switchToEmailForm,
|
||||
switchToSocialLogin
|
||||
} = useFreeTierOnboarding()
|
||||
const isWebView = isEmbeddedWebView()
|
||||
|
||||
const navigateToLogin = async () => {
|
||||
await router.push({ name: 'cloud-login', query: route.query })
|
||||
|
||||
119
src/utils/webviewDetection.test.ts
Normal file
119
src/utils/webviewDetection.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { isEmbeddedWebView } from '@/utils/webviewDetection'
|
||||
|
||||
describe('isEmbeddedWebView', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('Android WebView', () => {
|
||||
it('detects Android WebView with wv token', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-G991B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.0.0 Mobile Safari/537.36'
|
||||
expect(isEmbeddedWebView(ua)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not flag regular Chrome on Android', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
|
||||
expect(isEmbeddedWebView(ua)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('iOS WKWebView', () => {
|
||||
it('detects iOS WKWebView (AppleWebKit without Safari/)', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'
|
||||
expect(isEmbeddedWebView(ua)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not flag regular Safari on iOS', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
|
||||
expect(isEmbeddedWebView(ua)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not flag Chrome on iOS (CriOS)', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148'
|
||||
expect(isEmbeddedWebView(ua)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not flag Firefox on iOS (FxiOS)', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/120.0 Mobile/15E148'
|
||||
expect(isEmbeddedWebView(ua)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('social app in-app browsers', () => {
|
||||
it('detects Facebook (FBAN)', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBAV/400.0]'
|
||||
expect(isEmbeddedWebView(ua)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects Instagram', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 300.0'
|
||||
expect(isEmbeddedWebView(ua)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects TikTok', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36 TikTok/30.0'
|
||||
expect(isEmbeddedWebView(ua)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects Line', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Line/13.0'
|
||||
expect(isEmbeddedWebView(ua)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects Snapchat', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Snapchat/12.0'
|
||||
expect(isEmbeddedWebView(ua)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('regular desktop browsers', () => {
|
||||
it('does not flag Chrome desktop', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
expect(isEmbeddedWebView(ua)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not flag Firefox desktop', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0'
|
||||
expect(isEmbeddedWebView(ua)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not flag Safari desktop', () => {
|
||||
const ua =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15'
|
||||
expect(isEmbeddedWebView(ua)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty string', () => {
|
||||
expect(isEmbeddedWebView('')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('JS bridge detection', () => {
|
||||
it('detects webkit.messageHandlers bridge', () => {
|
||||
vi.stubGlobal('webkit', { messageHandlers: {} })
|
||||
expect(isEmbeddedWebView('')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects ReactNativeWebView bridge', () => {
|
||||
vi.stubGlobal('ReactNativeWebView', { postMessage: vi.fn() })
|
||||
expect(isEmbeddedWebView('')).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
57
src/utils/webviewDetection.ts
Normal file
57
src/utils/webviewDetection.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Detects whether the app is running inside an embedded webview.
|
||||
*
|
||||
* Google blocks OAuth via `signInWithPopup` in embedded webviews,
|
||||
* returning a 403 `disallowed_useragent` error (policy since 2021).
|
||||
* This utility is used to hide the Google SSO button in those contexts.
|
||||
*
|
||||
* Detection covers:
|
||||
* • Android WebView (`wv` token in UA)
|
||||
* • iOS WKWebView (has `AppleWebKit` but lacks `Safari/`)
|
||||
* • Social app in-app browsers (Facebook, Instagram, TikTok, etc.)
|
||||
* • JS bridge objects (`window.webkit.messageHandlers`, `ReactNativeWebView`)
|
||||
*/
|
||||
|
||||
const SOCIAL_APP_PATTERNS =
|
||||
/FBAN|FBAV|Instagram|Line\/|Snapchat|TikTok|musical_ly/i
|
||||
|
||||
function isAndroidWebView(ua: string): boolean {
|
||||
return /\bwv\b/.test(ua) && /Android/.test(ua)
|
||||
}
|
||||
|
||||
function isIOSWebView(ua: string): boolean {
|
||||
if (!/AppleWebKit/i.test(ua)) return false
|
||||
if (/Safari\//i.test(ua)) return false
|
||||
if (/CriOS|FxiOS|OPiOS|EdgiOS/i.test(ua)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function isSocialAppBrowser(ua: string): boolean {
|
||||
return SOCIAL_APP_PATTERNS.test(ua)
|
||||
}
|
||||
|
||||
function hasWebViewBridge(): boolean {
|
||||
try {
|
||||
const win = globalThis as Record<string, unknown>
|
||||
if (
|
||||
typeof win.webkit === 'object' &&
|
||||
win.webkit !== null &&
|
||||
typeof (win.webkit as Record<string, unknown>).messageHandlers ===
|
||||
'object'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (win.ReactNativeWebView != null) return true
|
||||
} catch {
|
||||
// Access to bridge objects may throw in sandboxed contexts
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function isEmbeddedWebView(ua: string = navigator.userAgent): boolean {
|
||||
if (isSocialAppBrowser(ua)) return true
|
||||
if (isAndroidWebView(ua)) return true
|
||||
if (isIOSWebView(ua)) return true
|
||||
if (hasWebViewBridge()) return true
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user