Skip to content
Open
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
200 changes: 40 additions & 160 deletions src/Plugins/Compilers/IOSPluginCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Native\Mobile\Exceptions\PluginConflictException;
use Native\Mobile\Plugins\Plugin;
use Native\Mobile\Plugins\PluginHookRunner;
use Native\Mobile\Plugins\PluginRegistry;
use Native\Mobile\Plugins\SwiftSourceFilter;
use Native\Mobile\Support\PlistDocument;
use Native\Mobile\Support\Stub;

class IOSPluginCompiler
Expand Down Expand Up @@ -177,9 +179,6 @@ public function compile(): void
// Write per-locale InfoPlist.strings for any localized permission entries
$this->writeInfoPlistLocalizations($allPlugins);

// Merge background modes into Info.plist
$this->mergeBackgroundModes($allPlugins);

// Merge entitlements from plugins
$this->mergeEntitlements($allPlugins);

Expand Down Expand Up @@ -413,50 +412,71 @@ protected function generateEmptyRendererRegistration(): void
}

/**
* Merge plugin Info.plist entries into main plist and simulator plist
* Merge every plugin's Info.plist contributions into the device and
* simulator plists: a resources/ios/Info.plist file, the manifest's
* info_plist entries and its background modes, with app-level
* overrides applied last so they always win over plugins.
*/
protected function mergeInfoPlistEntries(Collection $plugins): void
{
// Both device and simulator Info.plist files need plugin entries
$plistPaths = [
$this->iosProjectPath.'/NativePHP/Info.plist',
$this->iosProjectPath.'/NativePHP-simulator-Info.plist',
];

$appOverrides = $this->getAppInfoPlistOverrides();

foreach ($plistPaths as $plistPath) {
if (! $this->files->exists($plistPath)) {
continue;
}

$plist = $this->files->get($plistPath);
$plist = $this->openPlist($plistPath);

foreach ($plugins as $plugin) {
// First check for Info.plist file
$pluginPlistPath = $plugin->path.'/resources/ios/Info.plist';

if ($this->files->exists($pluginPlistPath)) {
$pluginPlist = $this->files->get($pluginPlistPath);
$plist = $this->mergePlists($plist, $pluginPlist);
$this->mergeIntoPlist($plist, $this->openPlist($pluginPlistPath)->all());
}

// Also merge info_plist entries from nativephp.json
$infoPlistEntries = $plugin->getIosInfoPlist();
if (! empty($infoPlistEntries)) {
$plist = $this->injectPlistEntries($plist, $infoPlistEntries);
$this->mergeIntoPlist($plist, $plugin->getIosInfoPlist());

if ($modes = $plugin->getIosBackgroundModes()) {
$this->mergeIntoPlist($plist, ['UIBackgroundModes' => $modes]);
}
}

// Apply app-level overrides last so they always win over plugins.
if (! empty($appOverrides)) {
$plist = $this->injectPlistEntries($plist, $appOverrides);
}
$this->mergeIntoPlist($plist, $this->getAppInfoPlistOverrides());

$this->files->put($plistPath, $plist);
$this->files->put($plistPath, $plist->toXml());
}
}

/**
* Open a plist, naming the file when it does not parse.
*/
protected function openPlist(string $path): PlistDocument
{
try {
return PlistDocument::fromXml($this->files->get($path));
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException("{$path}: {$e->getMessage()}", 0, $e);
}
}

/**
* Merge entries into a plist, resolving ${ENV_VAR} placeholders on the way in.
*/
protected function mergeIntoPlist(PlistDocument $plist, array $entries): void
{
array_walk_recursive($entries, function (&$value) {
if (is_string($value)) {
$value = $this->substituteEnvPlaceholders($value);
}
});

$plist->merge($entries);
}

/**
* Read app-level Info.plist overrides from config('nativephp.permissions').
*/
Expand Down Expand Up @@ -625,100 +645,6 @@ protected function registerKnownRegions(array $locales): void
}
}

/**
* Merge two plist files
*/
protected function mergePlists(string $main, string $plugin): string
{
// Extract key-value pairs from plugin plist
preg_match_all('/<key>([^<]+)<\/key>\s*<string>([^<]+)<\/string>/s', $plugin, $matches, PREG_SET_ORDER);

$entries = [];
foreach ($matches as $match) {
$entries[$match[1]] = $match[2];
}

return $this->injectPlistEntries($main, $entries);
}

/**
* Inject entries into plist
*/
protected function injectPlistEntries(string $plist, array $entries): string
{
foreach ($entries as $key => $value) {
// Check if key already exists
if (str_contains($plist, "<key>{$key}</key>")) {
if (is_array($value)) {
$plist = $this->mergeArrayEntry($plist, $key, $value);
} elseif (is_string($value)) {
$plist = $this->updateStringEntry($plist, $key, $this->substituteEnvPlaceholders($value));
}

continue;
}

// Handle array values
if (is_array($value)) {
$arrayContent = '';
foreach ($value as $item) {
$item = $this->substituteEnvPlaceholders($item);
$arrayContent .= "\n\t\t<string>{$item}</string>";
}
$entry = "\n\t<key>{$key}</key>\n\t<array>{$arrayContent}\n\t</array>";
} else {
// Handle string values - substitute placeholders
$value = $this->substituteEnvPlaceholders($value);
$entry = "\n\t<key>{$key}</key>\n\t<string>{$value}</string>";
}

// Add before closing </dict>
$plist = preg_replace(
'/(\s*<\/dict>\s*<\/plist>)/s',
$entry.'$1',
$plist,
1
);
}

return $plist;
}

/**
* Update an existing string entry's value in the plist
*/
protected function updateStringEntry(string $plist, string $key, string $value): string
{
$pattern = '/(<key>'.preg_quote($key, '/').'<\/key>\s*<string>)([^<]*)(<\/string>)/';

return preg_replace_callback($pattern, function ($matches) use ($value) {
return $matches[1].htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8').$matches[3];
}, $plist, 1);
}

/**
* Merge array values into an existing plist array entry
*/
protected function mergeArrayEntry(string $plist, string $key, array $values): string
{
$pattern = '/(<key>'.preg_quote($key, '/').'<\/key>\s*<array>)(.*?)(<\/array>)/s';

return preg_replace_callback($pattern, function ($matches) use ($values) {
$existingContent = $matches[2];
$newItems = '';

foreach ($values as $item) {
$item = $this->substituteEnvPlaceholders($item);
// Only add if not already present
if (! str_contains($existingContent, "<string>{$item}</string>")) {
$newItems .= "\n\t\t<string>{$item}</string>";
}
}

return $matches[1].$existingContent.$newItems.$matches[3];
}, $plist);
}

/**
* Substitute ${ENV_VAR} placeholders with actual environment values
*/
Expand All @@ -737,52 +663,6 @@ protected function substituteEnvPlaceholders(string $value): string
}, $value);
}

/**
* Merge background modes from plugins into Info.plist UIBackgroundModes array
*/
protected function mergeBackgroundModes(Collection $plugins): void
{
$backgroundModes = [];

foreach ($plugins as $plugin) {
$modes = $plugin->getIosBackgroundModes();
foreach ($modes as $mode) {
$backgroundModes[$mode] = true;
}
}

if (empty($backgroundModes)) {
return;
}

// Both device and simulator Info.plist files need background modes
$plistPaths = [
$this->iosProjectPath.'/NativePHP/Info.plist',
$this->iosProjectPath.'/NativePHP-simulator-Info.plist',
];

foreach ($plistPaths as $plistPath) {
if (! $this->files->exists($plistPath)) {
continue;
}

$plist = $this->files->get($plistPath);

// Check if UIBackgroundModes already exists
if (str_contains($plist, '<key>UIBackgroundModes</key>')) {
// Merge with existing array
$plist = $this->mergeArrayEntry($plist, 'UIBackgroundModes', array_keys($backgroundModes));
} else {
// Add new UIBackgroundModes array
$plist = $this->injectPlistEntries($plist, [
'UIBackgroundModes' => array_keys($backgroundModes),
]);
}

$this->files->put($plistPath, $plist);
}
}

/**
* Merge entitlements from plugins into the app's entitlements file
*/
Expand Down
Loading
Loading