mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
fix(ios): make the login WebView identify as Mobile Safari
accounts.spotify.com rejected iOS logins ("Oops, something went wrong") because
the WebView sent WKWebView's default User-Agent, which lacks Safari's
"Version/x ... Safari/604.1" and reads as an embedded browser. The random
User-Agent header never reached any request: the screen replaces the content
before it loads.
Have WebKit build Safari's exact UA (with its frozen OS version) via a probe
WKWebView using Safari's applicationNameForUserAgent, prime it at startup and
apply it through webSettings.customUserAgentString so every request uses it.
This commit is contained in:
parent
cc21568457
commit
bdf3655a23
@ -27,6 +27,7 @@ import androidx.compose.ui.window.ComposeUIViewController
|
||||
import com.yushosei.newpipe.extractor.NewPipe
|
||||
import com.yushosei.newpipe.util.DefaultDownloaderImpl
|
||||
import org.spotube.spotube.core.di.initKoin
|
||||
import org.spotube.spotube.core.webview.SafariUserAgent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -35,6 +36,7 @@ import org.koin.core.component.inject
|
||||
|
||||
fun MainViewController() = ComposeUIViewController(configure = {
|
||||
initKoin()
|
||||
SafariUserAgent.prime()
|
||||
}) { InitNewPipe { App() } }
|
||||
|
||||
@Composable
|
||||
|
||||
@ -1,41 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.webview
|
||||
|
||||
import dev.nucleusframework.webview.web.WebViewState
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.Foundation.NSDate
|
||||
import platform.Foundation.NSHTTPCookieStorage
|
||||
import platform.Foundation.distantPast
|
||||
import platform.WebKit.WKWebsiteDataStore
|
||||
import platform.WebKit.WKWebsiteDataTypeCookies
|
||||
import platform.WebKit.WKWebsiteDataTypeLocalStorage
|
||||
|
||||
actual fun platformWebviewConfig(webView: WebViewState, pluginId: String?) {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual suspend fun platformClearWebviewData(pluginId: String?) {
|
||||
if (pluginId == null) return
|
||||
|
||||
val dataStore = WKWebsiteDataStore.defaultDataStore()
|
||||
val dataTypes = setOf(WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage)
|
||||
dataStore.removeDataOfTypes(dataTypes, NSDate.distantPast) {}
|
||||
|
||||
NSHTTPCookieStorage.sharedHTTPCookieStorage.removeCookiesSinceDate(NSDate.distantPast)
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.webview
|
||||
|
||||
import dev.nucleusframework.webview.web.WebViewState
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import platform.Foundation.NSDate
|
||||
import platform.Foundation.NSHTTPCookieStorage
|
||||
import platform.Foundation.distantPast
|
||||
import platform.WebKit.WKWebsiteDataStore
|
||||
import platform.WebKit.WKWebsiteDataTypeCookies
|
||||
import platform.WebKit.WKWebsiteDataTypeLocalStorage
|
||||
|
||||
actual fun platformWebviewConfig(webView: WebViewState, pluginId: String?) {
|
||||
// Applied to the WKWebView itself, so it covers redirects and XHR too, not just the first request.
|
||||
SafariUserAgent.value?.let { webView.webSettings.customUserAgentString = it }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
actual suspend fun platformClearWebviewData(pluginId: String?) {
|
||||
if (pluginId == null) return
|
||||
|
||||
val dataStore = WKWebsiteDataStore.defaultDataStore()
|
||||
val dataTypes = setOf(WKWebsiteDataTypeCookies, WKWebsiteDataTypeLocalStorage)
|
||||
dataStore.removeDataOfTypes(dataTypes, NSDate.distantPast) {}
|
||||
|
||||
NSHTTPCookieStorage.sharedHTTPCookieStorage.removeCookiesSinceDate(NSDate.distantPast)
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.webview
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.readValue
|
||||
import platform.CoreGraphics.CGRectZero
|
||||
import platform.UIKit.UIDevice
|
||||
import platform.UIKit.UIUserInterfaceIdiomPad
|
||||
import platform.WebKit.WKWebView
|
||||
import platform.WebKit.WKWebViewConfiguration
|
||||
|
||||
/**
|
||||
* Mobile Safari's exact User-Agent on this device.
|
||||
*
|
||||
* WKWebView's default UA ends in `Mobile/15E148` without Safari's `Version/x … Safari/604.1`,
|
||||
* which sites like accounts.spotify.com treat as an embedded browser ("Oops, something went
|
||||
* wrong"). Hand-building the string isn't safe either: Safari freezes the OS part (iOS 26
|
||||
* reports `iPhone OS 18_7`). So WebKit builds it: a probe WKWebView gets Safari's application
|
||||
* name and reports `navigator.userAgent`, which is cached for every web view.
|
||||
*/
|
||||
internal object SafariUserAgent {
|
||||
var value: String? = null
|
||||
private set
|
||||
|
||||
// Kept alive until its async evaluation calls back.
|
||||
private var probe: WKWebView? = null
|
||||
|
||||
/** Must run on the main thread (WebKit); call once at startup. */
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
fun prime() {
|
||||
if (value != null || probe != null) return
|
||||
val config = WKWebViewConfiguration().apply { applicationNameForUserAgent = safariApplicationName() }
|
||||
probe = WKWebView(frame = CGRectZero.readValue(), configuration = config).also { webView ->
|
||||
webView.evaluateJavaScript("navigator.userAgent") { result, _ ->
|
||||
value = result as? String
|
||||
probe = null
|
||||
Logger.withTag("SafariUserAgent").i { "WebView User-Agent: $value" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What Safari appends after WebKit's base UA; iPad Safari sends the desktop-class form.
|
||||
private fun safariApplicationName(): String {
|
||||
val version = UIDevice.currentDevice.systemVersion.split('.').take(2).joinToString(".")
|
||||
return if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
||||
"Version/$version Safari/605.1.15"
|
||||
} else {
|
||||
"Version/$version Mobile/15E148 Safari/604.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user