diff --git a/composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.kt b/composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.kt
index 1e72f984..e3b722f6 100644
--- a/composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.kt
+++ b/composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.kt
@@ -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
diff --git a/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/webview/PlatformWebviewConfig.ios.kt b/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/webview/PlatformWebviewConfig.ios.kt
index 9b1ed717..c465732c 100644
--- a/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/webview/PlatformWebviewConfig.ios.kt
+++ b/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/webview/PlatformWebviewConfig.ios.kt
@@ -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 .
- */
-
-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 .
+ */
+
+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)
}
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/webview/SafariUserAgent.kt b/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/webview/SafariUserAgent.kt
new file mode 100644
index 00000000..4607265d
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/org/spotube/spotube/core/webview/SafariUserAgent.kt
@@ -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 .
+ */
+
+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"
+ }
+ }
+}