From a3aa85f3fdf7813e10cb1a9cd8b65f0e10009c0f Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Tue, 24 Mar 2026 17:53:03 -0400 Subject: [PATCH 1/4] Update Deep Link Router --- .../xcode/NativePHP/DeepLinkRouter.swift | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/resources/xcode/NativePHP/DeepLinkRouter.swift b/resources/xcode/NativePHP/DeepLinkRouter.swift index f29a887..06c86ae 100644 --- a/resources/xcode/NativePHP/DeepLinkRouter.swift +++ b/resources/xcode/NativePHP/DeepLinkRouter.swift @@ -67,10 +67,13 @@ final class DeepLinkRouter { DebugLogger.shared.log("🔗 Normalized to: \(newURLString)") - // 3. Either redirect immediately or store for later + // 3. Either navigate immediately or store for later if isWebViewReady && isPhpReady { - DebugLogger.shared.log("🔗 Both ready, redirecting immediately") - redirectToURL(newURLString) + // App is already running — use Inertia router for SPA navigation + // This prevents Inertia from returning raw JSON on subsequent navigations + // (e.g. second OAuth login after logout) + DebugLogger.shared.log("🔗 Both ready, navigating with Inertia") + navigateWithInertia(normalizedRoute) } else { DebugLogger.shared.log("🔗 Not ready, storing as pending URL") // Store the URL to handle once both WebView and PHP are ready @@ -87,4 +90,18 @@ final class DeepLinkRouter { ) DebugLogger.shared.log("🔗 redirectToURL() notification posted successfully") } + + /// Navigate using Inertia router when the app is already running. + /// Uses the path (not the full php:// URL) so window.router.visit() works correctly. + private func navigateWithInertia(_ path: String) { + DebugLogger.shared.log("🔗 navigateWithInertia() posting notification for path: \(path)") + DispatchQueue.main.async { + NotificationCenter.default.post( + name: .navigateWithInertiaNotification, + object: nil, + userInfo: ["path": path] + ) + } + DebugLogger.shared.log("🔗 navigateWithInertia() notification posted") + } } From 07e11ca4220946fbb2c7a0590502d12ae082c658 Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Wed, 25 Mar 2026 14:27:48 -0400 Subject: [PATCH 2/4] Fix stale POST data causing duplicate bridge calls + notification URL navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHPBridge.getLastPostData() was never cleared after consumption, causing subsequent requests to re-execute stale bridge call payloads. This manifested as duplicate notifications when navigating after a notification tap. Also adds notification URL navigation support in onNewIntent — when a notification with a URL is tapped and intent.data is null (launcher intent doesn't carry deep link data), falls back to reading the notification_url extra and navigating via Inertia/location.href. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/com/nativephp/mobile/bridge/PHPBridge.kt | 4 +++- .../src/main/java/com/nativephp/mobile/ui/MainActivity.kt | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt index 16983c8..30b3f74 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt @@ -285,7 +285,9 @@ class PHPBridge(private val context: Context) { } fun getLastPostData(): String? { - return lastPostData + val data = lastPostData + lastPostData = null // Clear after consumption to prevent stale data + return data } fun getLaravelPath(): String { diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt index 5aee8de..1f228cc 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt @@ -278,6 +278,14 @@ class MainActivity : FragmentActivity(), WebViewProvider { super.onNewIntent(intent) handleDeepLinkIntent(intent) + // If deep link didn't fire but we have a notification URL, navigate via Inertia + if (intent.data == null) { + val notificationUrl = intent.getStringExtra("notification_url") + if (!notificationUrl.isNullOrEmpty()) { + navigateWithInertia(notificationUrl) + } + } + // Post lifecycle event for plugins intent.data?.let { uri -> NativePHPLifecycle.post( From 1476dfdf4af79f3100e9ec0ad96fa424c40ae9cb Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Thu, 26 Mar 2026 08:59:46 -0400 Subject: [PATCH 3/4] Add ephemeral PHP runtime for plugin background execution Add a generic ephemeral TSRM context (boot/artisan/shutdown) to php_bridge.c that any plugin can use for background PHP work via WorkManager. Supports both hot path (app alive) and cold path (WorkManager cold start). Add JNI declarations to PHPBridge.kt and init function support to the plugin compiler stub template. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/src/main/cpp/php_bridge.c | 179 ++++++++++++++++++ .../com/nativephp/mobile/bridge/PHPBridge.kt | 5 + .../PluginBridgeFunctionRegistration.kt.stub | 2 + .../Compilers/AndroidPluginCompiler.php | 48 ++++- 4 files changed, 229 insertions(+), 5 deletions(-) diff --git a/resources/androidstudio/app/src/main/cpp/php_bridge.c b/resources/androidstudio/app/src/main/cpp/php_bridge.c index d1ad509..c3e49a4 100644 --- a/resources/androidstudio/app/src/main/cpp/php_bridge.c +++ b/resources/androidstudio/app/src/main/cpp/php_bridge.c @@ -18,6 +18,7 @@ jobject g_bridge_instance = NULL; extern jint InitializeBridgeJNI(JNIEnv* env); static void safe_php_embed_shutdown(void); static void worker_embed_shutdown(void); +static void ephemeral_embed_shutdown(void); int android_header_handler(sapi_header_struct *sapi_header, sapi_header_op_enum op, sapi_headers_struct *sapi_headers); // Global state @@ -69,6 +70,11 @@ static void (*jni_output_callback_ptr)(const char *) = NULL; static int worker_initialized = 0; static pthread_mutex_t g_worker_mutex = PTHREAD_MUTEX_INITIALIZER; +// Ephemeral state — generic background TSRM context for plugin use +static int ephemeral_initialized = 0; +static int ephemeral_cold_booted = 0; +static pthread_mutex_t g_ephemeral_mutex = PTHREAD_MUTEX_INITIALIZER; + /** * Configure the embed SAPI module with host-registered functions. * Must be called before each php_embed_init(). @@ -1050,6 +1056,174 @@ JNIEXPORT void JNICALL native_worker_shutdown(JNIEnv *env, jobject thiz) { pthread_mutex_unlock(&g_worker_mutex); } +// ============================================================================ +// Ephemeral PHP Runtime — separate TSRM context for plugin background work +// ============================================================================ +// Generic background PHP context that any plugin can use (e.g. background tasks, +// scheduled jobs). Supports both hot path (app alive) and cold path (WorkManager +// cold start after app killed). + +static int ephemeral_embed_init(void) { + if (php_initialized) { + // Hot path: persistent runtime is alive, allocate a TSRM thread context + LOGI("ephemeral_embed_init: hot path — using existing TSRM"); + + ts_resource(0); + setup_embed_module(); + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + LOGE("ephemeral_embed_init: module startup failed"); + return FAILURE; + } + + if (php_request_startup() == FAILURE) { + LOGE("ephemeral_embed_init: request startup failed"); + return FAILURE; + } + + ephemeral_cold_booted = 0; + + LOGI("ephemeral_embed_init: hot path ready"); + return SUCCESS; + } + + // Cold path: WorkManager started the process after app was killed. + // No persistent runtime exists — do a full php_embed_init(). + LOGI("ephemeral_embed_init: cold path — full PHP bootstrap"); + + setenv("NATIVEPHP_RUNNING", "true", 1); + setenv("APP_URL", "http://127.0.0.1", 1); + setenv("ASSET_URL", "http://127.0.0.1/_assets/", 1); + setenv("APP_RUNNING_IN_CONSOLE", "true", 1); + setenv("PHP_SELF", "/ephemeral", 1); + setenv("HTTP_HOST", "127.0.0.1", 1); + + setup_embed_module(); + if (php_embed_init(0, NULL) != SUCCESS) { + LOGE("ephemeral_embed_init: cold path php_embed_init() FAILED"); + return FAILURE; + } + sapi_module.header_handler = android_header_handler; + ephemeral_cold_booted = 1; + + LOGI("ephemeral_embed_init: cold path ready"); + return SUCCESS; +} + +static void ephemeral_embed_shutdown(void) { + if (ephemeral_cold_booted) { + LOGI("ephemeral_embed_shutdown: cold path — full php_embed_shutdown"); + safe_php_embed_shutdown(); + } else { + LOGI("ephemeral_embed_shutdown: hot path — thread cleanup only"); + php_request_shutdown(NULL); + ts_free_thread(); + } + LOGI("ephemeral_embed_shutdown: done"); +} + +JNIEXPORT jint JNICALL native_ephemeral_boot(JNIEnv *env, jobject thiz, jstring jBootstrapPath) { + pthread_mutex_lock(&g_ephemeral_mutex); + + if (ephemeral_initialized) { + LOGI("ephemeral_boot: already initialized, skipping"); + pthread_mutex_unlock(&g_ephemeral_mutex); + return 0; + } + + const char *bootstrapPath = (*env)->GetStringUTFChars(env, jBootstrapPath, NULL); + LOGI("ephemeral_boot: initializing with bootstrap=%s", bootstrapPath); + + clear_collected_output(); + + if (ephemeral_embed_init() != SUCCESS) { + LOGE("ephemeral_boot: ephemeral_embed_init() FAILED"); + (*env)->ReleaseStringUTFChars(env, jBootstrapPath, bootstrapPath); + pthread_mutex_unlock(&g_ephemeral_mutex); + return -1; + } + + zend_first_try { + zend_activate_modules(); + zend_file_handle fileHandle; + zend_stream_init_filename(&fileHandle, bootstrapPath); + php_execute_script(&fileHandle); + } zend_end_try(); + + char *ephemeral_boot_output = get_collected_output(); + if (ephemeral_boot_output && strstr(ephemeral_boot_output, "FATAL") != NULL) { + LOGE("ephemeral_boot: bootstrap produced errors: %.200s", ephemeral_boot_output); + } + + ephemeral_initialized = 1; + LOGI("ephemeral_boot: ephemeral PHP interpreter ready"); + + (*env)->ReleaseStringUTFChars(env, jBootstrapPath, bootstrapPath); + pthread_mutex_unlock(&g_ephemeral_mutex); + return 0; +} + +JNIEXPORT jstring JNICALL native_ephemeral_artisan(JNIEnv *env, jobject thiz, jstring jCommand) { + pthread_mutex_lock(&g_ephemeral_mutex); + + if (!ephemeral_initialized) { + LOGE("ephemeral_artisan: ephemeral runtime not initialized!"); + pthread_mutex_unlock(&g_ephemeral_mutex); + return (*env)->NewStringUTF(env, "Ephemeral runtime not initialized."); + } + + const char *command = (*env)->GetStringUTFChars(env, jCommand, NULL); + LOGI("ephemeral_artisan: %s", command); + + clear_collected_output(); + + setenv("APP_RUNNING_IN_CONSOLE", "true", 1); + + char eval_code[4096]; + snprintf(eval_code, sizeof(eval_code), + "try {\n" + " echo \\Native\\Mobile\\Runtime::artisan('%s');\n" + "} catch (\\Throwable $e) {\n" + " echo 'Ephemeral artisan error: ' . $e->getMessage();\n" + "}\n", + command); + + zend_first_try { + zend_eval_string(eval_code, NULL, "ephemeral_artisan"); + } zend_end_try(); + + setenv("APP_RUNNING_IN_CONSOLE", "false", 1); + + (*env)->ReleaseStringUTFChars(env, jCommand, command); + + char *ephemeral_output = get_collected_output(); + jstring result = (*env)->NewStringUTF(env, ephemeral_output ? ephemeral_output : ""); + pthread_mutex_unlock(&g_ephemeral_mutex); + return result; +} + +JNIEXPORT void JNICALL native_ephemeral_shutdown(JNIEnv *env, jobject thiz) { + pthread_mutex_lock(&g_ephemeral_mutex); + + if (!ephemeral_initialized) { + LOGI("ephemeral_shutdown: not initialized, nothing to do"); + pthread_mutex_unlock(&g_ephemeral_mutex); + return; + } + + LOGI("ephemeral_shutdown: shutting down ephemeral interpreter"); + + zend_first_try { + zend_eval_string("\\Native\\Mobile\\Runtime::shutdown();", NULL, "ephemeral_shutdown"); + } zend_end_try(); + + ephemeral_embed_shutdown(); + ephemeral_initialized = 0; + + LOGI("ephemeral_shutdown: done"); + pthread_mutex_unlock(&g_ephemeral_mutex); +} + static JNINativeMethod gMethods[] = { // PHPBridge {"nativeExecuteScript", "(Ljava/lang/String;)Ljava/lang/String;", (void *) native_execute_script}, @@ -1078,6 +1252,11 @@ static JNINativeMethod gMethods[] = { {"nativeWorkerBoot","(Ljava/lang/String;)I",(void *) native_worker_boot}, {"nativeWorkerArtisan","(Ljava/lang/String;)Ljava/lang/String;",(void *) native_worker_artisan}, {"nativeWorkerShutdown","()V",(void *) native_worker_shutdown}, + + // Ephemeral runtime (background tasks via WorkManager) methods + {"nativeEphemeralBoot","(Ljava/lang/String;)I",(void *) native_ephemeral_boot}, + {"nativeEphemeralArtisan","(Ljava/lang/String;)Ljava/lang/String;",(void *) native_ephemeral_artisan}, + {"nativeEphemeralShutdown","()V",(void *) native_ephemeral_shutdown}, }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt index 30b3f74..500641b 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.kt @@ -63,6 +63,11 @@ class PHPBridge(private val context: Context) { external fun nativeWorkerArtisan(command: String): String external fun nativeWorkerShutdown() + // Ephemeral runtime JNI methods — generic background TSRM context for plugin use + external fun nativeEphemeralBoot(bootstrapPath: String): Int + external fun nativeEphemeralArtisan(command: String): String + external fun nativeEphemeralShutdown() + @Volatile private var runtimeInitialized = false diff --git a/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub b/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub index 7cf04a3..ed52365 100644 --- a/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub +++ b/resources/stubs/android/PluginBridgeFunctionRegistration.kt.stub @@ -11,5 +11,7 @@ import com.nativephp.mobile.bridge.BridgeFunctionRegistry fun registerPluginBridgeFunctions(activity: FragmentActivity, context: Context) { val registry = BridgeFunctionRegistry.shared +{{ INIT_FUNCTIONS }} + {{ REGISTRATIONS }} } \ No newline at end of file diff --git a/src/Plugins/Compilers/AndroidPluginCompiler.php b/src/Plugins/Compilers/AndroidPluginCompiler.php index a65b5da..b80f87b 100644 --- a/src/Plugins/Compilers/AndroidPluginCompiler.php +++ b/src/Plugins/Compilers/AndroidPluginCompiler.php @@ -146,7 +146,7 @@ public function compile(): void return; } - // Check if there are any plugins with Android bridge functions + // Check if there are any plugins with Android bridge functions or init functions $hasAndroidFunctions = $allPlugins->filter(function (Plugin $p) { foreach ($p->getBridgeFunctions() as $function) { if (! empty($function['android'])) { @@ -157,6 +157,10 @@ public function compile(): void return false; })->isNotEmpty(); + $hasInitFunctions = $allPlugins->filter(function (Plugin $p) { + return $p->getAndroidInitFunction() !== null; + })->isNotEmpty(); + // Ensure generated directory exists $this->files->ensureDirectoryExists($this->generatedPath); @@ -164,8 +168,8 @@ public function compile(): void $allPlugins->filter(fn (Plugin $p) => $p->hasAndroidCode()) ->each(fn (Plugin $plugin) => $this->copyPluginSources($plugin)); - // Generate the registration file - if ($hasAndroidFunctions) { + // Generate the bridge function registration file + if ($hasAndroidFunctions || $hasInitFunctions) { $this->generateBridgeFunctionRegistration($allPlugins); } else { $this->generateEmptyRegistration(); @@ -265,8 +269,10 @@ protected function sanitizeKotlinName(string $name): string protected function generateBridgeFunctionRegistration(Collection $plugins): void { $registrations = []; + $initFunctions = []; foreach ($plugins as $plugin) { + // Collect bridge function registrations foreach ($plugin->getBridgeFunctions() as $function) { if (empty($function['android'])) { continue; @@ -279,9 +285,18 @@ protected function generateBridgeFunctionRegistration(Collection $plugins): void 'params' => $function['android_params'] ?? ['activity'], ]; } + + // Collect init functions + $initFunction = $plugin->getAndroidInitFunction(); + if ($initFunction) { + $initFunctions[] = [ + 'function' => $initFunction, + 'plugin' => $plugin->name, + ]; + } } - $content = $this->renderRegistrationTemplate($registrations); + $content = $this->renderRegistrationTemplate($registrations, $initFunctions); $path = $this->generatedPath.'/PluginBridgeFunctionRegistration.kt'; $this->files->put($path, $content); @@ -291,7 +306,7 @@ protected function generateBridgeFunctionRegistration(Collection $plugins): void /** * Render the Kotlin registration file */ - protected function renderRegistrationTemplate(array $registrations): string + protected function renderRegistrationTemplate(array $registrations, array $initFunctions = []): string { // Build imports from the android class paths in nativephp.json $imports = collect($registrations) @@ -302,6 +317,18 @@ protected function renderRegistrationTemplate(array $registrations): string ->map(fn ($package) => "import {$package}") ->implode("\n"); + // Add imports for init functions (top-level Kotlin functions need full path import) + $initImports = collect($initFunctions) + ->pluck('function') + ->unique() + ->sort() + ->map(fn ($func) => "import {$func}") + ->implode("\n"); + + if ($initImports) { + $imports = $imports ? $imports."\n".$initImports : $initImports; + } + $registerCalls = collect($registrations) ->map(function ($reg) { $className = $this->extractClassName($reg['class']); @@ -312,9 +339,20 @@ protected function renderRegistrationTemplate(array $registrations): string }) ->implode("\n\n"); + $initCalls = collect($initFunctions) + ->map(function ($init) { + // Extract just the function name from the full path + $parts = explode('.', $init['function']); + $funcName = end($parts); + + return " // Plugin: {$init['plugin']}\n {$funcName}(context)"; + }) + ->implode("\n\n"); + return Stub::make('android/PluginBridgeFunctionRegistration.kt.stub') ->replaceAll([ 'IMPORTS' => $imports, + 'INIT_FUNCTIONS' => $initCalls, 'REGISTRATIONS' => $registerCalls, ]) ->render(); From 10f42d2790587c08638322e668043a2fc6f18c6e Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Wed, 1 Apr 2026 15:33:14 -0400 Subject: [PATCH 4/4] Add push notification deep link and data message support (#71) * Add push notification deep link and data message support - Handle FCM data payload deep links (url/link keys) in handleDeepLinkIntent for cold-start push notification navigation on Android - Add didReceiveRemoteNotification to AppDelegate for iOS data-only message delivery (silent push via content-available) - Fix IOSPluginCompiler to handle boolean values in nativephp.json info_plist entries (needed for FirebaseAppDelegateProxyEnabled: false) - Add clearBadge method to PushNotifications --- .../com/nativephp/mobile/ui/MainActivity.kt | 26 ++++++++++++++++++- resources/xcode/NativePHP/AppDelegate.swift | 14 ++++++++++ src/Facades/PushNotifications.php | 1 + src/Plugins/Compilers/IOSPluginCompiler.php | 4 +++ src/PushNotifications.php | 12 +++++++++ 5 files changed, 56 insertions(+), 1 deletion(-) diff --git a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt index 1f228cc..dc7b952 100644 --- a/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt +++ b/resources/androidstudio/app/src/main/java/com/nativephp/mobile/ui/MainActivity.kt @@ -306,7 +306,7 @@ class MainActivity : FragmentActivity(), WebViewProvider { } private fun handleDeepLinkIntent(intent: Intent?) { - // Check for notification URL extra (from local notification taps) + // Check for notification URL extra (from local notification taps or foreground push) val notificationUrl = intent?.getStringExtra("notification_url") if (!notificationUrl.isNullOrEmpty()) { Log.d("DeepLink", "🔔 Notification URL: $notificationUrl") @@ -320,6 +320,30 @@ class MainActivity : FragmentActivity(), WebViewProvider { return } + // Check for deep link URL from FCM data payload (background/killed push notifications) + val fcmUrl = intent?.getStringExtra("url") ?: intent?.getStringExtra("link") + if (!fcmUrl.isNullOrEmpty()) { + Log.d("DeepLink", "🔔 FCM deep link URL: $fcmUrl") + val uri = android.net.Uri.parse(fcmUrl) + val scheme = uri.scheme + val route = if (scheme != null && scheme != "http" && scheme != "https") { + val host = uri.host ?: "" + val path = uri.path ?: "" + val query = uri.query?.let { "?$it" } ?: "" + if (host.isNotEmpty()) "/$host$path$query" else "$path$query" + } else { + fcmUrl + } + pendingDeepLink = route + if (::laravelEnv.isInitialized && ::webViewManager.isInitialized) { + val fullUrl = "http://127.0.0.1$route" + Log.d("DeepLink", "🚀 Loading FCM deep link immediately: $fullUrl") + webView.loadUrl(fullUrl) + pendingDeepLink = null + } + return + } + val uri = intent?.data ?: return Log.d("DeepLink", "🌐 Received deep link: $uri") diff --git a/resources/xcode/NativePHP/AppDelegate.swift b/resources/xcode/NativePHP/AppDelegate.swift index 0295973..01c1bca 100644 --- a/resources/xcode/NativePHP/AppDelegate.swift +++ b/resources/xcode/NativePHP/AppDelegate.swift @@ -107,4 +107,18 @@ class AppDelegate: NSObject, UIApplicationDelegate { DeepLinkRouter.shared.handle(url: url) return true } + + // Handle remote notifications (data-only messages, silent push) + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + NotificationCenter.default.post( + name: .didReceiveRemoteNotification, + object: nil, + userInfo: ["payload": userInfo] + ) + completionHandler(.newData) + } } diff --git a/src/Facades/PushNotifications.php b/src/Facades/PushNotifications.php index af81018..b8a317a 100644 --- a/src/Facades/PushNotifications.php +++ b/src/Facades/PushNotifications.php @@ -9,6 +9,7 @@ * @method static PendingPushNotificationEnrollment enroll() * @method static string|null checkPermission() * @method static string|null getToken() + * @method static void clearBadge() * * @see \Native\Mobile\PushNotifications */ diff --git a/src/Plugins/Compilers/IOSPluginCompiler.php b/src/Plugins/Compilers/IOSPluginCompiler.php index 8878986..9cf888d 100644 --- a/src/Plugins/Compilers/IOSPluginCompiler.php +++ b/src/Plugins/Compilers/IOSPluginCompiler.php @@ -367,6 +367,10 @@ protected function injectPlistEntries(string $plist, array $entries): string $arrayContent .= "\n\t\t{$item}"; } $entry = "\n\t{$key}\n\t{$arrayContent}\n\t"; + } elseif (is_bool($value)) { + // Handle boolean values + $boolTag = $value ? '' : ''; + $entry = "\n\t{$key}\n\t{$boolTag}"; } else { // Handle string values - substitute placeholders $value = $this->substituteEnvPlaceholders($value); diff --git a/src/PushNotifications.php b/src/PushNotifications.php index a2636d2..90fafb3 100644 --- a/src/PushNotifications.php +++ b/src/PushNotifications.php @@ -64,4 +64,16 @@ public function getToken(): ?string return null; } + + /** + * Clear the app icon badge number + */ + public function clearBadge(): void + { + if (! function_exists('nativephp_call')) { + return; + } + + nativephp_call('PushNotification.ClearBadge', '{}'); + } }