Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -16,6 +16,7 @@ import com.acsbendi.requestinspectorwebview.RequestInspectorWebViewClient
import com.nativephp.mobile.bridge.PHPBridge
import com.nativephp.mobile.ui.MainActivity
import com.nativephp.mobile.ui.NativeUIState
import com.nativephp.mobile.utils.NativeActionCoordinator
import org.json.JSONObject
import com.nativephp.mobile.security.LaravelSecurity

Expand Down Expand Up @@ -350,6 +351,15 @@ class WebViewManager(

// Inject JavaScript to capture form submissions and AJAX requests
injectJavaScript(view)

// Open the dispatch gate now that a NativePHP page is loaded and the
// native-event listener / fetch wrappers are installed. Queued events
// from native code that fired during cold boot will replay here.
if (url.contains("127.0.0.1")) {
(context as? MainActivity)?.let {
NativeActionCoordinator.markWebViewReady(it)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ class MainActivity : FragmentActivity(), WebViewProvider {
super.onCreate(savedInstanceState)
instance = this

// Reset the dispatch readiness gate. Without this, a cold boot triggered by
// Android killing the app (e.g. after the user opened Settings from a
// permission flow) inherits a stale "ready" flag from the previous process
// and dispatches against a WebView that hasn't loaded its URL yet.
NativeActionCoordinator.markWebViewNotReady()

// Android 15 edge-to-edge compatibility fix
WindowCompat.setDecorFitsSystemWindows(window, false)

Expand Down Expand Up @@ -562,50 +568,64 @@ class MainActivity : FragmentActivity(), WebViewProvider {
private fun injectJavaScript(view: WebView) {
val jsCode = """
(function() {
// Add platform identifier class
document.body.classList.add('nativephp-android');
function nativephpSetup() {
// document.body may be null if this runs before DOM parsing begins
// (e.g. against about:blank while the real URL is still loading).
if (!document.body) {
return false;
}

// 🌐 Native event bridge
const listeners = {};
// Add platform identifier class
document.body.classList.add('nativephp-android');

const Native = {
on: function(eventName, callback) {
if (!listeners[eventName]) {
listeners[eventName] = [];
}
listeners[eventName].push(callback);
},
off: function(eventName, callback) {
if (listeners[eventName]) {
listeners[eventName] = listeners[eventName].filter(cb => cb !== callback);
}
},
dispatch: function(eventName, payload) {
const cbs = listeners[eventName] || [];
cbs.forEach(cb => cb(payload, eventName));
},
openDrawer: function() {
if (window.AndroidBridge) {
window.AndroidBridge.openDrawer();
// 🌐 Native event bridge
const listeners = {};

const Native = {
on: function(eventName, callback) {
if (!listeners[eventName]) {
listeners[eventName] = [];
}
listeners[eventName].push(callback);
},
off: function(eventName, callback) {
if (listeners[eventName]) {
listeners[eventName] = listeners[eventName].filter(cb => cb !== callback);
}
},
dispatch: function(eventName, payload) {
const cbs = listeners[eventName] || [];
cbs.forEach(cb => cb(payload, eventName));
},
openDrawer: function() {
if (window.AndroidBridge) {
window.AndroidBridge.openDrawer();
}
}
}
};
};

window.Native = Native;
window.Native = Native;

document.addEventListener("native-event", function (e) {
// Normalize event names by removing leading backslashes
let eventName = e.detail.event.replace(/^(\\\\)+/, '');
const payload = e.detail.payload;
document.addEventListener("native-event", function (e) {
// Normalize event names by removing leading backslashes
let eventName = e.detail.event.replace(/^(\\\\)+/, '');
const payload = e.detail.payload;

// Dispatch with normalized event name
Native.dispatch(eventName, payload);
// Dispatch with normalized event name
Native.dispatch(eventName, payload);

// Also dispatch to Livewire if available
if (window.Livewire && typeof window.Livewire.dispatch === 'function') {
window.Livewire.dispatch('native:' + eventName, payload);
}
});
// Also dispatch to Livewire if available
if (window.Livewire && typeof window.Livewire.dispatch === 'function') {
window.Livewire.dispatch('native:' + eventName, payload);
}
});

return true;
}

if (!nativephpSetup()) {
document.addEventListener('DOMContentLoaded', nativephpSetup);
}
})();
"""
view.evaluateJavascript(jsCode, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,40 +54,56 @@ class NativeActionCoordinator : Fragment() {
}

private fun dispatch(event: String, payloadJson: String) {
// Queue if the WebView hasn't loaded a NativePHP page yet. Without this gate,
// dispatches during cold boot run against about:blank — the relative fetch
// fails to parse, the native-event listener isn't attached, and the event
// is silently dropped (or worse, leaves the WebView in a broken state).
if (!webViewReady) {
Log.d("NativeActionCoordinator", "⏳ WebView not ready — queueing event: $event")
synchronized(pendingEvents) {
pendingEvents.add(event to payloadJson)
}
return
}

Log.d("JSFUNC", "native:$event");
Log.d("JSFUNC", "$payloadJson");
val eventForJs = event.replace("\\", "\\\\")
val js = """
(function () {
const payload = $payloadJson;

const detail = { event: "$eventForJs", payload };

document.dispatchEvent(new CustomEvent("native-event", { detail }));

if (window.Livewire && typeof window.Livewire.dispatch === 'function') {
window.Livewire.dispatch("native:$eventForJs", payload);
try {
const payload = $payloadJson;

const detail = { event: "$eventForJs", payload };

document.dispatchEvent(new CustomEvent("native-event", { detail }));

if (window.Livewire && typeof window.Livewire.dispatch === 'function') {
window.Livewire.dispatch("native:$eventForJs", payload);
}

fetch('http://127.0.0.1/_native/api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
event: "$eventForJs",
payload: payload
})
}).then(response => response.json())
.then(data => {
if (data.message && data.message.includes("Unknown named parameter")) {
console.log("API Event Dispatch: Parameter issue detected");
} else {
console.log("API Event Dispatch Success");
}
})
.catch(error => console.error("API Event Dispatch Error:", error && error.message));
} catch (e) {
console.error("API Event Dispatch threw:", e && e.message);
}

fetch('/_native/api/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
event: "$eventForJs",
payload: payload
})
}).then(response => response.json())
.then(data => {
if (data.message && data.message.includes("Unknown named parameter")) {
console.log("API Event Dispatch: Parameter issue detected");
} else {
console.log("API Event Dispatch Success");
}
})
.catch(error => console.error("API Event Dispatch Error:", error.message));
})();
""".trimIndent()

Expand All @@ -98,6 +114,14 @@ class NativeActionCoordinator : Fragment() {


companion object {
// Events dispatched before the first NativePHP page has finished loading are
// queued here and replayed once the WebView is ready. The flag is reset in
// MainActivity.onCreate so cold boots start in the "not ready" state.
private val pendingEvents = mutableListOf<Pair<String, String>>()

@Volatile
private var webViewReady: Boolean = false

fun install(activity: FragmentActivity): NativeActionCoordinator =
activity.supportFragmentManager.findFragmentByTag("NativeActionCoordinator") as? NativeActionCoordinator
?: NativeActionCoordinator().also {
Expand All @@ -115,5 +139,36 @@ class NativeActionCoordinator : Fragment() {
val coordinator = install(activity)
coordinator.dispatch(event, payloadJson)
}

/**
* Signal that the WebView has finished loading a NativePHP page and is safe
* to receive dispatches. Queued events from before this point are replayed.
*/
fun markWebViewReady(activity: FragmentActivity) {
webViewReady = true

val toReplay = synchronized(pendingEvents) {
val copy = pendingEvents.toList()
pendingEvents.clear()
copy
}

if (toReplay.isEmpty()) return

Log.d("NativeActionCoordinator", "▶️ Replaying ${toReplay.size} queued event(s)")
val coordinator = install(activity)
toReplay.forEach { (event, payloadJson) ->
coordinator.dispatch(event, payloadJson)
}
}

/**
* Mark the WebView as not ready. Called from MainActivity.onCreate so that
* a cold boot — including the one triggered when Android kills the app while
* the user is in Settings — restarts in the queueing state.
*/
fun markWebViewNotReady() {
webViewReady = false
}
}
}
Loading