This skill provides Kotlin and Swift code patterns for NativePHP plugins. Use when the user asks about "bridge function pattern", "kotlin bridge", "swift bridge", "BridgeFunction class", "BridgeResponse", "execute method", "parameter extraction", "dispatch event", "NativeActionCoordinator", "LaravelBridge", "Activity pattern", "ViewController pattern", threading in native code, or how to write native code for NativePHP plugins.
/plugin marketplace add NativePHP/ClaudePlugins/plugin install nativephp-plugin-dev@nativephp-mobile-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill provides complete, production-ready patterns for Kotlin (Android) and Swift (iOS) bridge functions in NativePHP plugins.
BridgeResponse is a REAL helper object that EXISTS in NativePHP and MUST be used.
Do NOT write code that returns plain Map<String, Any> or [String: Any] directly. Always use:
import com.nativephp.mobile.bridge.BridgeResponse
// Success
return BridgeResponse.success(mapOf("key" to "value"))
// Error (simple)
return BridgeResponse.error("Error message")
// Error (with code)
return BridgeResponse.error("ERROR_CODE", "Error message")
// Success
return BridgeResponse.success(data: ["key": "value"])
// Error (simple)
return BridgeResponse.error("Error message")
// Error (with code)
return BridgeResponse.error(code: "ERROR_CODE", message: "Error message")
BridgeResponse is defined in:
com.nativephp.mobile.bridge.BridgeResponse (BridgeResponse object)The official NativePHP plugin stubs use BridgeResponse. Follow this pattern.
package com.myvendor.plugins.myplugin
import androidx.fragment.app.FragmentActivity
import android.content.Context
import com.nativephp.mobile.bridge.BridgeFunction
import com.nativephp.mobile.bridge.BridgeResponse
object MyPluginFunctions {
/**
* Brief description of what this function does.
*
* Parameters:
* - paramName: Type - Description
*
* Returns:
* - resultKey: Type - Description
*/
class Execute(private val activity: FragmentActivity) : BridgeFunction {
override fun execute(parameters: Map<String, Any>): Map<String, Any> {
// 1. Extract and validate parameters
val param1 = parameters["param1"] as? String
?: return BridgeResponse.error("param1 is required")
// 2. Perform the native operation
try {
val result = performOperation(param1)
// 3. Return success response
return BridgeResponse.success(mapOf(
"result" to result
))
} catch (e: Exception) {
return BridgeResponse.error(e.message ?: "Unknown error")
}
}
private fun performOperation(param: String): String {
// Implementation
return "processed: $param"
}
}
// All bridge functions use FragmentActivity - get context from it if needed
class GetStatus(private val activity: FragmentActivity) : BridgeFunction {
override fun execute(parameters: Map<String, Any>): Map<String, Any> {
// If you need context: val context: Context = activity
return BridgeResponse.success(mapOf(
"status" to "ready",
"version" to "1.0.0"
))
}
}
}
Use your own vendor-namespaced package for your plugin code:
package com.myvendor.plugins.myplugin
Where myvendor is your vendor name and myplugin is your plugin name (lowercase, no hyphens - use underscores if needed). The plugins segment groups all your plugins together. This keeps your plugin code isolated from other plugins.
Use FragmentActivity for ALL bridge functions. The NativePHP bridge registration passes activity to all functions. You can always get Context from activity:
// CORRECT - works with the bridge registration
class MyFunction(private val activity: FragmentActivity) : BridgeFunction {
override fun execute(parameters: Map<String, Any>): Map<String, Any> {
// Get context when needed
val context: Context = activity
val prefs = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
// ...
}
}
Do NOT use Context as constructor parameter - the bridge registers functions with activity, not context.
// Required string
val name = parameters["name"] as? String
?: return BridgeResponse.error("name is required")
// Required number (always comes as Number)
val count = (parameters["count"] as? Number)?.toInt()
?: return BridgeResponse.error("count is required")
// Optional with default
val quality = (parameters["quality"] as? Number)?.toInt() ?: 80
val enabled = parameters["enabled"] as? Boolean ?: false
// Double/Float
val amount = (parameters["amount"] as? Number)?.toDouble() ?: 0.0
// Arrays/Lists
val items = (parameters["items"] as? List<*>)?.filterIsInstance<String>() ?: emptyList()
val numbers = (parameters["numbers"] as? List<*>)?.mapNotNull { (it as? Number)?.toInt() } ?: emptyList()
// Nested objects
val config = parameters["config"] as? Map<*, *>
val configValue = config?.get("key") as? String ?: "default"
// Success with data
return BridgeResponse.success(mapOf(
"path" to filePath,
"size" to fileSize,
"metadata" to mapOf(
"width" to width,
"height" to height
)
))
// Success with array
return BridgeResponse.success(mapOf(
"items" to listOf(
mapOf("id" to 1, "name" to "Item 1"),
mapOf("id" to 2, "name" to "Item 2")
)
))
// Simple error
return BridgeResponse.error("Something went wrong")
// Error with code
return BridgeResponse.error("FILE_NOT_FOUND", "The specified file does not exist")
Events notify PHP when async operations complete:
import android.os.Handler
import android.os.Looper
import org.json.JSONObject
import com.nativephp.mobile.NativeActionCoordinator
// MUST dispatch on main thread for JavaScript execution
Handler(Looper.getMainLooper()).post {
val payload = JSONObject().apply {
put("path", filePath)
put("mimeType", mimeType)
put("id", operationId)
}
NativeActionCoordinator.dispatchEvent(
activity,
"Vendor\\MyPlugin\\Events\\OperationCompleted",
payload.toString()
)
}
For long-running operations, return immediately and dispatch event when done:
import kotlinx.coroutines.*
import java.util.UUID
class AsyncOperation(private val activity: FragmentActivity) : BridgeFunction {
override fun execute(parameters: Map<String, Any>): Map<String, Any> {
val id = UUID.randomUUID().toString()
CoroutineScope(Dispatchers.IO).launch {
try {
val result = performLongOperation()
// Dispatch event on main thread
withContext(Dispatchers.Main) {
val payload = JSONObject().apply {
put("id", id)
put("result", result)
put("success", true)
}
NativeActionCoordinator.dispatchEvent(
activity,
"Vendor\\MyPlugin\\Events\\OperationCompleted",
payload.toString()
)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
val payload = JSONObject().apply {
put("id", id)
put("error", e.message)
put("success", false)
}
NativeActionCoordinator.dispatchEvent(
activity,
"Vendor\\MyPlugin\\Events\\OperationFailed",
payload.toString()
)
}
}
}
// Return immediately with tracking ID
return BridgeResponse.success(mapOf("id" to id))
}
private suspend fun performLongOperation(): String {
delay(1000) // Simulate work
return "completed"
}
}
For camera, scanners, complex UI:
import android.content.Intent
class OpenScanner(private val activity: FragmentActivity) : BridgeFunction {
override fun execute(parameters: Map<String, Any>): Map<String, Any> {
val config = parameters["config"] as? String
val intent = Intent(activity, ScannerActivity::class.java).apply {
putExtra("config", config)
}
activity.startActivity(intent)
return BridgeResponse.success(mapOf("launched" to true))
}
}
The Activity dispatches events when done:
class ScannerActivity : AppCompatActivity() {
private fun onScanComplete(result: String) {
Handler(Looper.getMainLooper()).post {
val payload = JSONObject().apply {
put("result", result)
}
NativeActionCoordinator.dispatchEvent(
this,
"Vendor\\MyPlugin\\Events\\ScanCompleted",
payload.toString()
)
}
finish()
}
}
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
class RequiresCamera(private val activity: FragmentActivity) : BridgeFunction {
override fun execute(parameters: Map<String, Any>): Map<String, Any> {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
return BridgeResponse.error("PERMISSION_DENIED", "Camera permission required")
}
// Proceed with camera operation
return performCameraOperation()
}
}
import Foundation
enum MyPluginFunctions {
/// Brief description of what this function does.
///
/// - Parameters:
/// - paramName: Description of parameter
///
/// - Returns: Dictionary containing:
/// - resultKey: Description of return value
class Execute: BridgeFunction {
func execute(parameters: [String: Any]) throws -> [String: Any] {
// 1. Extract and validate parameters
guard let param1 = parameters["param1"] as? String else {
return BridgeResponse.error("param1 is required")
}
// 2. Perform the native operation
do {
let result = try performOperation(param1)
// 3. Return success response
return BridgeResponse.success(data: [
"result": result
])
} catch {
return BridgeResponse.error(error.localizedDescription)
}
}
private func performOperation(_ param: String) throws -> String {
return "processed: \(param)"
}
}
class GetStatus: BridgeFunction {
func execute(parameters: [String: Any]) throws -> [String: Any] {
return BridgeResponse.success(data: [
"status": "ready",
"version": "1.0.0"
])
}
}
}
{Namespace}Functions.swift file per pluginenum as namespace container (prevents instantiation)class inside the enum// Required string
guard let name = parameters["name"] as? String else {
return BridgeResponse.error("name is required")
}
// Required number
guard let count = parameters["count"] as? Int else {
return BridgeResponse.error("count is required")
}
// Optional with default
let quality = parameters["quality"] as? Int ?? 80
let enabled = parameters["enabled"] as? Bool ?? false
// Double/Float
let amount = parameters["amount"] as? Double ?? 0.0
// Arrays
let items = parameters["items"] as? [String] ?? []
let numbers = parameters["numbers"] as? [Int] ?? []
// Nested dictionary
if let config = parameters["config"] as? [String: Any],
let configValue = config["key"] as? String {
// Use configValue
}
// Array of dictionaries
let people = parameters["people"] as? [[String: Any]] ?? []
for person in people {
if let name = person["name"] as? String,
let age = person["age"] as? Int {
// Process person
}
}
// Success with data
return BridgeResponse.success(data: [
"path": filePath,
"size": fileSize,
"metadata": [
"width": width,
"height": height
]
])
// Success with array
return BridgeResponse.success(data: [
"items": [
["id": 1, "name": "Item 1"],
["id": 2, "name": "Item 2"]
]
])
// Simple error
return BridgeResponse.error("Something went wrong")
// Error with code
return BridgeResponse.error(code: "FILE_NOT_FOUND", message: "The specified file does not exist")
// Dispatch on main thread (usually already there)
DispatchQueue.main.async {
let payload: [String: Any] = [
"path": filePath,
"mimeType": mimeType,
"id": operationId
]
LaravelBridge.shared.send?(
"Vendor\\MyPlugin\\Events\\OperationCompleted",
payload
)
}
class AsyncOperation: BridgeFunction {
func execute(parameters: [String: Any]) throws -> [String: Any] {
let id = UUID().uuidString
// Dispatch to background
DispatchQueue.global(qos: .userInitiated).async {
do {
let result = self.performLongOperation()
// Dispatch event back on main thread
DispatchQueue.main.async {
LaravelBridge.shared.send?(
"Vendor\\MyPlugin\\Events\\OperationCompleted",
[
"id": id,
"result": result,
"success": true
]
)
}
} catch {
DispatchQueue.main.async {
LaravelBridge.shared.send?(
"Vendor\\MyPlugin\\Events\\OperationFailed",
[
"id": id,
"error": error.localizedDescription,
"success": false
]
)
}
}
}
// Return immediately with tracking ID
return BridgeResponse.success(data: ["id": id])
}
private func performLongOperation() -> String {
Thread.sleep(forTimeInterval: 1.0) // Simulate work
return "completed"
}
}
// Modern Swift concurrency version
class ModernAsyncOperation: BridgeFunction {
func execute(parameters: [String: Any]) throws -> [String: Any] {
let id = UUID().uuidString
Task {
let result = await performAsyncWork()
await MainActor.run {
LaravelBridge.shared.send?(
"Vendor\\MyPlugin\\Events\\OperationCompleted",
["id": id, "result": result]
)
}
}
return BridgeResponse.success(data: ["id": id])
}
private func performAsyncWork() async -> String {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return "completed"
}
}
import UIKit
class OpenScanner: BridgeFunction {
func execute(parameters: [String: Any]) throws -> [String: Any] {
// Get the key window's root view controller
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController else {
return BridgeResponse.error("Cannot present view controller")
}
// Find the topmost presented controller
var topVC = rootVC
while let presented = topVC.presentedViewController {
topVC = presented
}
// Create and present your view controller
let scannerVC = ScannerViewController()
scannerVC.modalPresentationStyle = .fullScreen
DispatchQueue.main.async {
topVC.present(scannerVC, animated: true)
}
return BridgeResponse.success(data: ["presented": true])
}
}
class ScannerViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func onScanComplete(result: String) {
dismiss(animated: true) {
LaravelBridge.shared.send?(
"Vendor\\MyPlugin\\Events\\ScanCompleted",
["result": result]
)
}
}
}
import AVFoundation
class RequiresCamera: BridgeFunction {
func execute(parameters: [String: Any]) throws -> [String: Any] {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
return performCameraOperation()
case .notDetermined:
// Request permission asynchronously
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
LaravelBridge.shared.send?(
"Vendor\\MyPlugin\\Events\\PermissionResult",
["granted": granted, "permission": "camera"]
)
}
}
return BridgeResponse.success(data: ["pending": true])
case .denied, .restricted:
return BridgeResponse.error(
code: "PERMISSION_DENIED",
message: "Camera permission denied. Please enable in Settings."
)
@unknown default:
return BridgeResponse.error("Unknown permission status")
}
}
}
Synchronous (returns data immediately):
$status = nativephp_call('MyPlugin.GetStatus', []);
// Returns: ['status' => 'ready']
Asynchronous (returns ID, dispatches event later):
$result = nativephp_call('MyPlugin.StartLongOperation', ['data' => $data]);
// Returns: ['id' => 'uuid-here']
// Later: native:Vendor\MyPlugin\Events\OperationCompleted fires
Android:
Dispatchers.IO for I/O operationsHandler(Looper.getMainLooper()).post { }iOS:
DispatchQueue.global(qos: .userInitiated) for background workDispatchQueue.main.async { }When returning file paths to PHP:
// Android
return BridgeResponse.success(mapOf(
"path" to file.absolutePath // Full path like /data/data/com.app/files/photo.jpg
))
// iOS
return BridgeResponse.success(data: [
"path": fileURL.path // Full path like /var/mobile/.../Documents/photo.jpg
])
PHP can then use these paths directly for file operations.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.