Skip to content

Conversation

@dcalhoun
Copy link
Member

@dcalhoun dcalhoun commented Jan 16, 2026

What?

Adds a pull-based content recovery mechanism that allows the web editor to request the latest content from the native host during initialization.

Why?

Ref CMM-1123.

When the WebView reinitializes (due to OS memory pressure or page refresh), the editor previously loaded stale content from window.GBKit.post which was injected at original load time. The host apps have fresher content from autosave events (~1s intervals), but this wasn't being used on WebView recovery.

Related WordPress app PRs:

How?

Implements a pull model where the web editor always requests content from the native host during initialization:

  1. Web bridge (src/utils/bridge.js): Added requestLatestContent() function that calls the appropriate native bridge method
  2. iOS (EditorViewController.swift): Implements WKScriptMessageHandlerWithReply to respond to content requests via a new delegate method editorRequestsLatestContent
  3. Android (GutenbergView.kt): Adds LatestContentProvider interface and @JavascriptInterface method that returns JSON content
  4. Editor initialization (src/utils/editor.jsx): getPost() now requests content from native host first, falling back to window.GBKit.post only when bridge is unavailable

Testing Instructions

  1. Open a post with existing content in the editor
  2. Make edits to the content (wait 2+ seconds for autosave)
  3. Trigger a WebView refresh:
    • Safari DevTools: Cmd+R while WebView is inspected
    • iOS Simulator: Open Activity Monitor, force quit "com.apple.WebKit.WebContent"
    • Android: Chrome DevTools → three-dot menu → "Crash the renderer"
  4. Verify the editor recovers with the edited content, not the original

Accessibility Testing Instructions

No UI changes - this is a data recovery mechanism that happens during initialization.

Screenshots or screencast

N/A - No visual changes

🤖 Generated with Claude Code

Expose function for retrieving the latest content from the native host.
This is useful for ensuring the content is updated after WebView
refresh or re-initialization.
Enable the editor to request the latest content from the host.
Enable the editor to request the latest content from the host.
Ensure the editor always uses the latest content. This particularly
important for subsequent initialization events--e.g., WebView refresh.
When crossing actor boundaries, Swift requires type to conform to
`Sendable`, which `Any?` cannot. Constructing the dictionary outside of
the `MainActor` run avoids this incompatibility.
@dcalhoun
Copy link
Member Author

@claude

@claude
Copy link

claude bot commented Jan 16, 2026

Claude finished @dcalhoun's task in 1m 53s —— View job


Code Review Complete ✅

This PR implements a solid pull-based content recovery mechanism. The implementation is well-structured across all three platforms with comprehensive test coverage. Below is my detailed review.


Overall Assessment

Strengths:

  • ✅ Clear architecture with consistent patterns across iOS, Android, and web
  • ✅ Excellent test coverage (406 lines of new tests)
  • ✅ Well-documented code with helpful comments
  • ✅ Proper fallback behavior when bridge is unavailable
  • ✅ Demo implementations in both sample apps

Architecture: The pull model is the right approach here. Having the web layer request content rather than push it eliminates timing issues and makes the recovery mechanism more reliable.


Detailed Findings

1. Code Quality ⚠️

JavaScript (bridge.js:256-278)

  • Error handling uses debug() instead of error() for actual failures (violates CLAUDE.md logging guidelines)
  • iOS bridge check could be more defensive with runtime type validation
  • See inline comments for suggested improvements

Swift (EditorViewController.swift:665, 749)

  • Delegate method return is optional, which could mask integration issues in production
  • Return type doesn't distinguish between "no content" vs "error" scenarios
  • Consider adding debug logging when delegate is nil

Kotlin (GutenbergView.kt:672)

  • JSON serialization lacks error handling for edge cases
  • Could crash if content contains problematic characters
  • Should wrap in try-catch (see inline comment)

2. Security ✅

No security concerns identified:

  • ✅ Content is properly URL-encoded on both platforms
  • ✅ No injection vulnerabilities in bridge communication
  • ✅ JSON parsing has appropriate error handling (except one case noted above)
  • ✅ No authentication/authorization changes

3. Performance ✅

Performance impact is minimal and positive:

  • ✅ Async/await pattern prevents blocking UI thread
  • ✅ Single content request during initialization (no polling)
  • ✅ Fallback to cached GBKit.post is instant when bridge unavailable
  • ✅ No additional network requests

Measurement suggestion: Consider adding timing metrics to measure recovery speed in production.

4. Testing ⭐

Excellent test coverage:

  • ✅ 406 lines of new tests in bridge.test.js
  • ✅ Tests cover iOS bridge, Android bridge, fallbacks, and priority
  • ✅ Edge cases well covered (null, empty string, malformed JSON, errors)
  • ✅ Test for iOS priority over Android when both available

Minor suggestion: The priority test at line 182 is great but could use a comment explaining why iOS has priority.

5. Documentation ✅

Documentation is thorough:

  • ✅ JSDoc comments on new functions
  • ✅ Swift documentation comments on delegate method
  • ✅ Kotlin interface documentation
  • ✅ PR description explains the "why" clearly
  • ✅ Testing instructions are comprehensive

Recommendations

High Priority

  1. Fix logging per CLAUDE.md guidelines - Use error() for actual errors in bridge.js:262, 272
  2. Add error handling to Android JSON serialization - See inline comment on GutenbergView.kt:672

Medium Priority

  1. Improve error observability - Distinguish between "no content" vs "error" in iOS implementation
  2. Add resilience to editor initialization - Wrap getPost() in try-catch in editor.jsx:46

Low Priority

  1. Add debug logging - Log when delegate is nil to help integrators discover missing implementation
  2. Add runtime type checks - More defensive iOS bridge validation

Pre-Merge Checklist

Before merging, please ensure:

  • Run make format to format JavaScript code
  • Run make lint-js-fix to auto-fix linting errors
  • Run make lint-js to verify linting passes
  • Run make test-js to ensure all tests pass

(Per CLAUDE.md pre-commit checklist)


Summary

This is a well-implemented feature that solves a real problem. The architecture is sound, test coverage is excellent, and the code is generally high quality. The issues identified are mostly minor improvements around error handling and observability.

Recommendation: Approve with suggested improvements for error handling.


@dcalhoun dcalhoun added the [Type] Enhancement A suggestion for improvement. label Jan 16, 2026
}
}

return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling could be more informative

The error handling here returns null for any failure, which makes debugging difficult. Consider:

  1. Using the error() logger instead of debug() for actual errors (per CLAUDE.md guidelines)
  2. Distinguishing between "no content available" (expected) vs "bridge error" (unexpected)
if ( window.editorDelegate?.requestLatestContent ) {
    try {
        const result = window.editorDelegate.requestLatestContent();
        return result ? JSON.parse( result ) : null;
    } catch ( error ) {
        // Use error() for actual errors, not debug()
        error( 'Failed to request content from Android host', error );
        return null;
    }
}

This helps distinguish between legitimate "no content" scenarios vs actual errors.

* @return {Promise<{title: string, content: string}|null>} The latest content or null if unavailable.
*/
export async function requestLatestContent() {
if ( window.webkit?.messageHandlers?.requestLatestContent ) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS bridge check could be more defensive

The iOS check uses optional chaining but doesn't verify that postMessage is actually a function. While unlikely to fail, consider adding runtime type validation for robustness:

if ( window.webkit?.messageHandlers?.requestLatestContent?.postMessage && 
     typeof window.webkit.messageHandlers.requestLatestContent.postMessage === 'function' ) {

This prevents potential crashes if the bridge is partially initialized or corrupted.

}
}

fileprivate func controllerRequestsLatestContent(_ controller: GutenbergEditorController) -> (title: String, content: String)? {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making delegate method non-optional in production

Currently returns nil when no delegate is set, which is fine for the demo but could mask integration issues in production. Consider:

  1. Adding debug logging when delegate is nil
  2. Or making the delegate method required (remove the ? from protocol method return type) and having a default implementation

This ensures integrators are aware they need to implement content recovery.

delegate?.controllerRequestsLatestContent(self)
}

guard let content else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return type should be more explicit about "no content" vs "error"

Returning (nil, nil) for both "no content available" and error conditions makes it hard for the web layer to distinguish between:

  • Normal case: no persisted content exists yet
  • Error case: delegate failed to retrieve content

Consider returning (nil, "error message") when delegate is nil or using a more structured error response. This would help with debugging in production.

* @return JSON string with title and content fields, or null if unavailable.
*/
@JavascriptInterface
fun requestLatestContent(): String? {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON serialization lacks error handling

The JSON object creation could throw if title/content contain characters that need escaping or if there are encoding issues. Consider wrapping in try-catch:

@JavascriptInterface
fun requestLatestContent(): String? {
    val content = latestContentProvider?.getLatestContent() ?: return null
    return try {
        JSONObject().apply {
            put("title", content.title)
            put("content", content.content)
        }.toString()
    } catch (e: JSONException) {
        Log.e("GutenbergView", "Failed to serialize latest content", e)
        null
    }
}

This prevents crashes if content contains problematic characters.

} );

describe( 'priority', () => {
it( 'should prefer iOS bridge when both are available', async () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent test coverage! 🎉

The priority test correctly validates that iOS bridge takes precedence over Android when both are available. This is important for preventing unexpected behavior in edge cases.

Minor suggestion: Add a comment explaining why iOS has priority (perhaps for consistency with other bridge methods or historical reasons).

unregisterDisallowedBlocks( allowedBlockTypes );
const post = getPost();

const post = await getPost();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good async handling

The await here is correct, but consider adding error boundary or fallback behavior if getPost() throws. Currently an error would bubble up and potentially crash the app.

const post = await getPost().catch(error => {
    // Log error and return safe defaults
    console.error('Failed to get post content', error);
    return {
        id: -1,
        type: 'post',
        status: 'auto-draft',
        title: { raw: '' },
        content: { raw: '' },
    };
});

This makes initialization more resilient to bridge failures.

@dcalhoun dcalhoun marked this pull request as draft January 19, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants