Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9974743
Initial plan
Copilot Dec 13, 2025
3ff1cd9
Add custom user agent to iOS and Android WebViews
Copilot Dec 13, 2025
e41c61b
Add tests for custom user agent functionality
Copilot Dec 13, 2025
a09b7e9
Set custom user agent in HTMLWebViewRenderer for consistency
Copilot Dec 13, 2025
24cc77e
Cache default user agent and improve test robustness
Copilot Dec 13, 2025
46febb2
Use public APIs and version constants in tests
Copilot Dec 13, 2025
96efbf7
Cache custom user agent to avoid repeated WebView instantiation
Copilot Dec 13, 2025
5602c97
Source version from package.json instead of hardcoding in native code
Copilot Dec 13, 2025
ca96457
Use logger utility and format generate-version.js
Copilot Dec 13, 2025
0419632
Fix Swift compilation: use enum namespace instead of extension
Copilot Dec 13, 2025
3bdab9d
Fix Swift module name conflict: rename to GBKVersion
Copilot Dec 13, 2025
edf1acb
Use applicationNameForUserAgent instead of customUserAgent
Copilot Dec 15, 2025
1ad006f
Convert tests to Swift Testing and verify navigator.userAgent
Copilot Dec 15, 2025
d183fa5
Simplify test assertion using optional chaining
Copilot Dec 15, 2025
d6ac478
Fix tests
jkmassel Dec 15, 2025
920788b
task: Include GutenbergKit user agent for native requests
dcalhoun Dec 21, 2025
4b639a4
refactor: Align version module naming
dcalhoun Dec 21, 2025
69bb9fe
test: Fix erroneously inverted test assertion
dcalhoun Dec 21, 2025
28dc6aa
refactor: Add generated version files to .gitignore
dcalhoun Dec 21, 2025
d8e2b30
fix: Repair `applicationNameForUserAgent` usage
dcalhoun Dec 21, 2025
e2e6ef4
refactor: Rename to avoid module name conflict
dcalhoun Dec 21, 2025
5996461
fix: Set user agent configuration before creating WebView
dcalhoun Dec 21, 2025
c3bf069
test: Ensure generated version files are present for test runs
dcalhoun Dec 21, 2025
7544b3b
refactor: Track GutenbergKit version files in Git
dcalhoun Dec 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class EditorAssetsLibrary(
try {
connection.requestMethod = "GET"

val defaultUserAgent = System.getProperty("http.agent") ?: ""
connection.setRequestProperty("User-Agent", "$defaultUserAgent GutenbergKit/${GutenbergKitVersion.VERSION}")

// Set headers from configuration
if (configuration.authHeader.isNotEmpty()) {
connection.setRequestProperty("Authorization", configuration.authHeader)
Expand Down Expand Up @@ -102,6 +105,10 @@ class EditorAssetsLibrary(
val connection = URL(httpURL).openConnection() as HttpURLConnection
try {
connection.requestMethod = "GET"

val defaultUserAgent = System.getProperty("http.agent") ?: ""
connection.setRequestProperty("User-Agent", "$defaultUserAgent GutenbergKit/${GutenbergKitVersion.VERSION}")

connection.connectTimeout = 30000
connection.readTimeout = 30000

Expand Down Expand Up @@ -149,7 +156,7 @@ class EditorAssetsLibrary(
*/
fun cacheAssetInBackground(url: String) {
if (!shouldCacheUrl(url)) return

scope.launch {
try {
cacheAsset(url)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// This file is auto-generated by bin/generate-version.js
// Do not edit manually - changes will be overwritten

package org.wordpress.gutenberg

/**
* GutenbergKit version information.
*/
object GutenbergKitVersion {
/**
* The current version of GutenbergKit.
*/
const val VERSION = "0.11.1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ class GutenbergView : WebView {
this.settings.javaScriptCanOpenWindowsAutomatically = true
this.settings.javaScriptEnabled = true
this.settings.domStorageEnabled = true

// Set custom user agent
val defaultUserAgent = this.settings.userAgentString
this.settings.userAgentString = "$defaultUserAgent GutenbergKit/${GutenbergKitVersion.VERSION}"

this.addJavascriptInterface(this, "editorDelegate")
this.visibility = View.GONE

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,20 @@ class GutenbergViewTest {
assertEquals("File path callback should be null after reset",
null, gutenbergView.filePathCallback)
}

@Test
fun `initializeWebView sets custom user agent with GutenbergKit identifier`() {
// Given
val gutenbergView = GutenbergView(RuntimeEnvironment.getApplication())

// When
gutenbergView.initializeWebView()

// Then
val userAgent = gutenbergView.settings.userAgentString
assertTrue("User agent should contain GutenbergKit identifier",
userAgent.contains("GutenbergKit/"))
assertTrue("User agent should contain version number",
userAgent.contains("GutenbergKit/${GutenbergKitVersion.VERSION}"))
}
}
74 changes: 74 additions & 0 deletions bin/generate-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env node

/**
* Generate version files for iOS and Android from package.json
* This ensures a single source of truth for the version number
*/

/**
* External dependencies
*/
import { readFileSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

/**
* Internal dependencies
*/
import { info } from '../src/utils/logger.js';

const __filename = fileURLToPath( import.meta.url );
const __dirname = dirname( __filename );
const rootDir = join( __dirname, '..' );

// Read version from package.json
const packageJson = JSON.parse(
readFileSync( join( rootDir, 'package.json' ), 'utf8' )
);
const version = packageJson.version;

info( `Generating version files for version ${ version }` );

// Generate iOS version file
const iosVersionContent = `// This file is auto-generated by bin/generate-version.js
// Do not edit manually - changes will be overwritten

/// GutenbergKit version information.
public enum GutenbergKitVersion {
/// The current version of GutenbergKit.
public static let version = "${ version }"
}
`;

const iosVersionPath = join(
rootDir,
'ios/Sources/GutenbergKit/Sources/GutenbergKitVersion.swift'
);
writeFileSync( iosVersionPath, iosVersionContent, 'utf8' );
info( `Generated ${ iosVersionPath }` );

// Generate Android version file
const androidVersionContent = `// This file is auto-generated by bin/generate-version.js
// Do not edit manually - changes will be overwritten

package org.wordpress.gutenberg

/**
* GutenbergKit version information.
*/
object GutenbergKitVersion {
/**
* The current version of GutenbergKit.
*/
const val VERSION = "${ version }"
}
`;

const androidVersionPath = join(
rootDir,
'android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergKitVersion.kt'
);
writeFileSync( androidVersionPath, androidVersionContent, 'utf8' );
info( `Generated ${ androidVersionPath }` );

info( 'Version files generated successfully' );
13 changes: 13 additions & 0 deletions ios/Sources/GutenbergKit/Sources/EditorHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ public actor EditorHTTPClient: EditorHTTPClientProtocol {
case unknown(response: Data, statusCode: Int)
}

/// The base user agent string identifying the platform.
private static let baseUserAgent: String = {
let version = ProcessInfo.processInfo.operatingSystemVersion
#if os(iOS)
return "iOS/\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
#elseif os(macOS)
return "macOS/\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
#else
return "Darwin"
#endif
}()

private let urlSession: URLSessionProtocol
private let authHeader: String
private let delegate: EditorHTTPClientDelegate?
Expand Down Expand Up @@ -99,6 +111,7 @@ public actor EditorHTTPClient: EditorHTTPClientProtocol {
private func configureRequest(_ request: URLRequest) -> URLRequest {
var mutableRequest = request
mutableRequest.addValue(self.authHeader, forHTTPHeaderField: "Authorization")
mutableRequest.addValue("\(Self.baseUserAgent) GutenbergKit/\(GutenbergKitVersion.version)", forHTTPHeaderField: "User-Agent")

if let requestTimeout {
mutableRequest.timeoutInterval = requestTimeout
Expand Down
2 changes: 2 additions & 0 deletions ios/Sources/GutenbergKit/Sources/EditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro

self.bundleProvider.bind(to: config)

config.applicationNameForUserAgent = "GutenbergKit/\(GutenbergKitVersion.version)"

self.webView = GBWebView(frame: .zero, configuration: config)
self.webView.scrollView.keyboardDismissMode = .interactive

Expand Down
8 changes: 8 additions & 0 deletions ios/Sources/GutenbergKit/Sources/GutenbergKitVersion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is auto-generated by bin/generate-version.js
// Do not edit manually - changes will be overwritten

/// GutenbergKit version information.
public enum GutenbergKitVersion {
/// The current version of GutenbergKit.
public static let version = "0.11.1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ final class HTMLWebViewRenderer {
init() {
let config = WKWebViewConfiguration()
config.suppressesIncrementalRendering = true
config.applicationNameForUserAgent = "GutenbergKit/\(GutenbergKitVersion.version)"

// Create web view with small initial frame for off-screen rendering
// Frame will be adjusted per render based on viewport width and content height
Expand Down
57 changes: 57 additions & 0 deletions ios/Tests/GutenbergKitTests/EditorHTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,63 @@ struct EditorHTTPClientTests {
#expect(lastCall.request.value(forHTTPHeaderField: "Authorization") == authHeader)
#expect(lastCall.request.httpShouldHandleCookies == false)
}

// MARK: - User-Agent Header Tests

@Test("perform() sets User-Agent header with GutenbergKit identifier")
func performSetsUserAgentHeader() async throws {
let spySession = SpyURLSession()
let client = EditorHTTPClient(
urlSession: spySession,
authHeader: "Bearer token"
)

let request = URLRequest(url: URL(string: "https://example.com/wp-json/wp/v2/posts")!)
_ = try await client.perform(request)

let capturedRequest = try #require(spySession.lastCapturedRequest)
let userAgent = try #require(capturedRequest.value(forHTTPHeaderField: "User-Agent"))
#expect(userAgent.contains("GutenbergKit/"))
#expect(userAgent.contains(GutenbergKitVersion.version))
}

@Test("download() sets User-Agent header with GutenbergKit identifier")
func downloadSetsUserAgentHeader() async throws {
let spySession = SpyURLSession()
let client = EditorHTTPClient(
urlSession: spySession,
authHeader: "Bearer token"
)

let request = URLRequest(url: URL(string: "https://example.com/wp-content/file.js")!)
_ = try await client.download(request)

let capturedRequest = try #require(spySession.lastCapturedRequest)
let userAgent = try #require(capturedRequest.value(forHTTPHeaderField: "User-Agent"))
#expect(userAgent.contains("GutenbergKit/"))
#expect(userAgent.contains(GutenbergKitVersion.version))
}

@Test("User-Agent header includes platform identifier")
func userAgentIncludesPlatformIdentifier() async throws {
let spySession = SpyURLSession()
let client = EditorHTTPClient(
urlSession: spySession,
authHeader: "Bearer token"
)

let request = URLRequest(url: URL(string: "https://example.com/wp-json/wp/v2/posts")!)
_ = try await client.perform(request)

let capturedRequest = try #require(spySession.lastCapturedRequest)
let userAgent = try #require(capturedRequest.value(forHTTPHeaderField: "User-Agent"))

#if os(iOS)
#expect(userAgent.contains("iOS/"))
#elseif os(macOS)
#expect(userAgent.contains("macOS/"))
#endif
}
}

fileprivate extension EditorResponseData {
Expand Down
19 changes: 19 additions & 0 deletions ios/Tests/GutenbergKitTests/GBWebViewTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Testing
import WebKit
@testable import GutenbergKit

struct GBWebViewTests {

@MainActor
func testApplicationNameForUserAgent() async throws {
let result = try await GBWebView().evaluateJavaScript("navigator.userAgent")
let string = try #require(result as? String)

#expect(string.hasSuffix("GutenbergKit/\(GutenbergKitVersion.version)"))
}

func testVersionConstantExists() {
#expect(!GutenbergKitVersion.version.isEmpty, "Version constant should not be empty")
#expect(GutenbergKitVersion.version.contains("."), "Version should be in semantic versioning format")
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
"dev:tools": "react-devtools",
"build": "vite --emptyOutDir build",
"format": "prettier --write .",
"generate-version": "node bin/generate-version.js",
"lint:js": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"lint:js:fix": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0 --fix",
"postinstall": "patch-package && npm run prep-translations",
"postinstall": "patch-package && npm run prep-translations && npm run generate-version",
"prep-translations": "node bin/prep-translations.js",
"preview": "vite preview --host",
"test:unit": "vitest run",
Expand Down