diff --git a/.env.example b/.env.example
index 4e73bab..1076b71 100644
--- a/.env.example
+++ b/.env.example
@@ -64,11 +64,23 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
-# AI Provider Keys
-OPENAI_API_KEY=
-ANTHROPIC_API_KEY=
-GEMINI_API_KEY=
+# AI Provider Keys (Required for functionality)
+# Get your OpenAI API key from: https://platform.openai.com/api-keys
+OPENAI_API_KEY=your-openai-api-key-here
# Realtime API Configuration
VITE_OPENAI_API_KEY="${OPENAI_API_KEY}"
VITE_REALTIME_RELAY_URL=wss://localhost:8080/realtime
+
+# NativePHP/Electron Configuration
+# Change this to your own app identifier (reverse domain format)
+NATIVEPHP_APP_ID=com.yourcompany.yourapp
+# NATIVEPHP_APP_VERSION=DEBUG
+
+# Apple Developer Code Signing (Required for macOS distribution)
+# Only needed if you want to build signed, distributable macOS apps
+# Get these from your Apple Developer Account: https://developer.apple.com/account/
+# IMPORTANT: Never commit real values to version control!
+NATIVEPHP_APPLE_ID=your-apple-id@example.com
+NATIVEPHP_APPLE_ID_PASS=your-app-specific-password
+NATIVEPHP_APPLE_TEAM_ID=YOUR_10_CHAR_TEAM_ID
diff --git a/.gitignore b/.gitignore
index cc07774..e78f041 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,24 @@ yarn-error.log
/.zed
/dist
.DS_Store
+
+# Security & Certificates - Never commit these!
+*.cer
+*.p12
+*.mobileprovision
+*.keychain
+*.keychain-db
+**/certificates/
+**/signing/
+.env.local
+.env.*.local
+*.pem
+*.key.pub
+keychain_password.txt
+apple_certificates/
+
+# Build artifacts that may contain sensitive data
+/build/certificates/
+/build/signing/
+/build/*.cer
+/build/*.p12
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 061381f..ffc9bc3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -87,9 +87,10 @@ Unsure where to begin contributing? You can start by looking through these issue
php artisan migrate --database=nativephp
```
-5. **Add your OpenAI API key to `.env`:**
+5. **Add your API keys to `.env`:**
```
- OPENAI_API_KEY=your-api-key-here
+ OPENAI_API_KEY=your-openai-api-key-here
+ NATIVEPHP_APP_ID=com.yourcompany.yourapp
```
6. **Run the development server:**
@@ -97,6 +98,56 @@ Unsure where to begin contributing? You can start by looking through these issue
composer dev
```
+## 🔐 Code Signing & Security
+
+### For Contributors (Development)
+
+**You don't need Apple Developer certificates for contributing!** The project automatically creates unsigned builds for development, which are perfect for testing your changes.
+
+If you see warnings like this, it's completely normal:
+```
+⚠️ No code signing certificate found - creating unsigned build
+💡 To create signed builds, install an Apple Developer certificate in Keychain
+```
+
+### Security Guidelines
+
+**❌ NEVER commit these:**
+- `.env` file with real credentials
+- Certificate files (`.cer`, `.p12`)
+- Apple Developer credentials
+- Real API keys
+
+**✅ SAFE to commit:**
+- `.env.example` (template only)
+- Source code
+- Build scripts (auto-detect certificates)
+- Documentation
+
+### Security Hardening
+
+This project follows security best practices:
+
+**Minimal Entitlements**: The `entitlements.plist` file uses the principle of least privilege, requesting only:
+- `com.apple.security.device.audio-input` - For audio input access
+- `com.apple.security.device.microphone` - For microphone access
+- `com.apple.security.device.screen-capture` - Required for ScreenCaptureKit system audio capture
+
+**Removed Dangerous Entitlements**: We explicitly avoid these high-risk permissions:
+- `com.apple.security.cs.allow-jit` - Unnecessary JIT compilation
+- `com.apple.security.cs.allow-unsigned-executable-memory` - Memory injection risk
+- `com.apple.security.cs.disable-executable-page-protection` - Memory corruption risk
+- `com.apple.security.cs.disable-library-validation` - Library injection risk
+
+### Before Committing
+
+Always verify no sensitive data:
+```bash
+git status
+git diff --cached
+grep -r "your-actual-api-key" . --exclude-dir=vendor
+```
+
## Pull Request Process
1. **Before submitting:**
diff --git a/README.md b/README.md
index 044245a..750784e 100644
--- a/README.md
+++ b/README.md
@@ -113,6 +113,6 @@ This project is licensed under the [MIT License](LICENSE) with the [Commons Clau
- ✅ **Modify it** to fit your needs
- ✅ **Self-host it** on your own infrastructure
- ❌ **Cannot sell** as a hosted service or SaaS product
-- ❌ **Cannot resell** without a commercial agreement
+- ❌ **Cannot resell**
-For commercial licensing inquiries, please reach out via [Discord](https://discord.gg/PhPMPrxcKw) or create an issue.
\ No newline at end of file
+For any questions, please reach out via [Discord](https://discord.gg/PhPMPrxcKw).
diff --git a/app/Http/Middleware/CheckOnboarding.php b/app/Http/Middleware/CheckOnboarding.php
index 1952f69..4c491fa 100644
--- a/app/Http/Middleware/CheckOnboarding.php
+++ b/app/Http/Middleware/CheckOnboarding.php
@@ -3,6 +3,8 @@
namespace App\Http\Middleware;
use App\Services\ApiKeyService;
+use App\Services\OnboardingStateService;
+use App\Services\AudioCapturePermissionService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -10,10 +12,17 @@
class CheckOnboarding
{
protected ApiKeyService $apiKeyService;
+ protected OnboardingStateService $onboardingService;
+ protected AudioCapturePermissionService $permissionService;
- public function __construct(ApiKeyService $apiKeyService)
- {
+ public function __construct(
+ ApiKeyService $apiKeyService,
+ OnboardingStateService $onboardingService,
+ AudioCapturePermissionService $permissionService
+ ) {
$this->apiKeyService = $apiKeyService;
+ $this->onboardingService = $onboardingService;
+ $this->permissionService = $permissionService;
}
/**
@@ -23,7 +32,7 @@ public function __construct(ApiKeyService $apiKeyService)
*/
public function handle(Request $request, Closure $next): Response
{
- // Routes that should be accessible without API key
+ // Routes that should be accessible without completed onboarding
$excludedRoutes = [
'onboarding',
'api-keys.edit',
@@ -31,6 +40,13 @@ public function handle(Request $request, Closure $next): Response
'api-keys.destroy',
'api.openai.status',
'api.openai.api-key.store',
+ 'api.onboarding.status',
+ 'api.onboarding.step',
+ 'api.onboarding.complete',
+ 'api.permissions.screen-recording.status',
+ 'api.permissions.screen-recording.request',
+ 'api.permissions.screen-recording.check',
+ 'api.open-external',
'appearance',
];
@@ -44,10 +60,32 @@ public function handle(Request $request, Closure $next): Response
return $next($request);
}
- // Check if API key exists
- if (!$this->apiKeyService->hasApiKey()) {
- // If not on onboarding page and no API key, redirect to onboarding
+ // Check if API key exists (Step 1)
+ $hasApiKey = $this->apiKeyService->hasApiKey();
+
+ // Check current system permissions (Step 2) - Always check real system state
+ $permissionCheck = $this->permissionService->checkScreenRecordingPermission();
+ $hasPermission = $permissionCheck['granted'] ?? false;
+
+ // Check if complete onboarding is finished (Step 3)
+ $isOnboardingComplete = $this->onboardingService->isOnboardingComplete();
+
+ // If any step is missing, redirect to onboarding
+ if (!$hasApiKey || !$hasPermission || !$isOnboardingComplete) {
+ // If not on onboarding page, redirect to onboarding
if ($request->route() && $request->route()->getName() !== 'onboarding') {
+ // Update onboarding state based on what's missing
+ if (!$hasApiKey) {
+ // No API key - handle API key removal
+ $this->onboardingService->handleApiKeyRemoval();
+ } else if (!$hasPermission) {
+ // API key exists but no permission - handle permission revocation
+ $this->onboardingService->handlePermissionRevocation();
+ } else if (!$isOnboardingComplete) {
+ // Steps 1&2 complete but step 3 incomplete - handle incomplete onboarding
+ $this->onboardingService->handleIncompleteOnboarding();
+ }
+
return redirect()->route('onboarding');
}
}
diff --git a/app/Providers/NativeAppServiceProvider.php b/app/Providers/NativeAppServiceProvider.php
index b2657db..eb3384a 100644
--- a/app/Providers/NativeAppServiceProvider.php
+++ b/app/Providers/NativeAppServiceProvider.php
@@ -8,6 +8,7 @@
use Illuminate\Support\Facades\Schema;
use Native\Laravel\Contracts\ProvidesPhpIni;
use Native\Laravel\Facades\Window;
+use Native\Laravel\Facades\Screen;
class NativeAppServiceProvider implements ProvidesPhpIni
{
@@ -17,18 +18,21 @@ class NativeAppServiceProvider implements ProvidesPhpIni
*/
public function boot(): void
{
+ // Get responsive window dimensions (75% of screen size)
+ $dimensions = $this->getResponsiveWindowDimensions();
+
// Create an overlay window for sales assistant
Window::open()
->route('realtime-agent')
- ->width(1200)
- ->height(700)
- ->minWidth(400)
- ->minHeight(500)
+ ->width($dimensions['width'])
+ ->height($dimensions['height'])
+ ->minWidth($dimensions['minWidth'])
+ ->minHeight($dimensions['minHeight'])
->titleBarStyle('hidden')
->transparent()
->backgroundColor('#00000000')
->resizable()
- ->position(50, 50)
+ ->position(100)
->webPreferences([
'nodeIntegration' => true,
'contextIsolation' => false,
@@ -36,6 +40,7 @@ public function boot(): void
'backgroundThrottling' => false,
'sandbox' => false,
])
+ ->rememberState()
// Set window to floating panel level for better screen protection
->alwaysOnTop(false);
@@ -77,4 +82,71 @@ protected function seedDatabaseIfNeeded(): void
// The app will still work without seed data
}
}
+
+ /**
+ * Get responsive window dimensions based on screen size.
+ * Returns 75% of screen dimensions with sensible minimums.
+ */
+ protected function getResponsiveWindowDimensions(): array
+ {
+ try {
+ // Get primary display dimensions
+ $displays = null;
+ try {
+ $displays = Screen::displays();
+ } catch (\Throwable $e) {
+ // Screen facade not available in non-native environment
+ throw new \Exception('Screen information not available');
+ }
+
+ // Handle case where displays() returns null (non-native environment)
+ if ($displays === null || empty($displays)) {
+ throw new \Exception('Screen information not available');
+ }
+
+ $primaryDisplay = $displays[0] ?? null;
+
+ if (!$primaryDisplay) {
+ // Fallback to reasonable defaults if screen info unavailable
+ return [
+ 'width' => 1280,
+ 'height' => 720,
+ 'minWidth' => 400,
+ 'minHeight' => 500,
+ ];
+ }
+
+ // Get screen dimensions from workArea (excludes taskbars/docks)
+ $screenWidth = $primaryDisplay['workArea']['width'];
+ $screenHeight = $primaryDisplay['workArea']['height'];
+
+ // Calculate 75% of screen dimensions
+ $width = (int) ($screenWidth * 0.75);
+ $height = (int) ($screenHeight * 0.75);
+
+ // Use fixed minimum dimensions (original values)
+ $minWidth = 400;
+ $minHeight = 500;
+
+ // Ensure we don't exceed reasonable maximums (90% of screen)
+ $maxWidth = (int) ($screenWidth * 0.9);
+ $maxHeight = (int) ($screenHeight * 0.9);
+
+ return [
+ 'width' => min($width, $maxWidth),
+ 'height' => min($height, $maxHeight),
+ 'minWidth' => $minWidth,
+ 'minHeight' => $minHeight,
+ ];
+
+ } catch (\Exception $e) {
+ // Fallback to reasonable defaults if anything fails
+ return [
+ 'width' => 1280,
+ 'height' => 720,
+ 'minWidth' => 400,
+ 'minHeight' => 500,
+ ];
+ }
+ }
}
diff --git a/app/Services/AudioCapturePermissionService.php b/app/Services/AudioCapturePermissionService.php
new file mode 100644
index 0000000..b0a9861
--- /dev/null
+++ b/app/Services/AudioCapturePermissionService.php
@@ -0,0 +1,284 @@
+getAudioCaptureBinaryPath();
+
+ if (!$binaryPath) {
+ return [
+ 'granted' => false,
+ 'error' => 'Audio capture binary not found. Please rebuild the application.',
+ 'needs_rebuild' => true
+ ];
+ }
+
+ // Use the same "start" command that the working "Start Call" uses
+ $process = Process::input('{"command": "start"}' . PHP_EOL)
+ ->timeout(5)
+ ->start([$binaryPath]);
+
+ // Wait briefly for the process to start and check permission
+ $startTime = time();
+ $timeout = 5;
+
+ while (time() - $startTime < $timeout && $process->running()) {
+ $output = $process->latestOutput();
+ $errorOutput = $process->latestErrorOutput();
+
+ if (!empty($output)) {
+ $lines = explode("\n", $output);
+
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if (empty($line)) continue;
+
+ // Try to parse JSON response
+ if ($decoded = json_decode($line, true)) {
+ if (isset($decoded['type'])) {
+ if ($decoded['type'] === 'status' && $decoded['state'] === 'capturing') {
+ // Permission granted! Stop the process immediately
+ $process->signal(15); // SIGTERM
+ return [
+ 'granted' => true,
+ 'error' => null,
+ 'needs_rebuild' => false
+ ];
+ } elseif ($decoded['type'] === 'error') {
+ $process->signal(15); // SIGTERM
+ // Check if it's a permission error
+ if (isset($decoded['code']) && $decoded['code'] === 1001) {
+ return [
+ 'granted' => false,
+ 'error' => null,
+ 'needs_rebuild' => false
+ ];
+ }
+ return [
+ 'granted' => false,
+ 'error' => $decoded['message'] ?? 'Unknown error',
+ 'needs_rebuild' => false
+ ];
+ }
+ }
+ }
+ }
+ }
+
+ usleep(200000); // 200ms sleep
+ }
+
+ // Kill the process if it's still running
+ if ($process->running()) {
+ $process->signal(15); // SIGTERM
+ }
+
+ $result = $process->wait();
+
+ // Check final output if we didn't find status during loop
+ $allOutput = $result->output();
+ $lines = explode("\n", $allOutput);
+
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if (empty($line)) continue;
+
+ if ($decoded = json_decode($line, true)) {
+ if (isset($decoded['type'])) {
+ if ($decoded['type'] === 'status' && $decoded['state'] === 'capturing') {
+ return [
+ 'granted' => true,
+ 'error' => null,
+ 'needs_rebuild' => false
+ ];
+ } elseif ($decoded['type'] === 'error' && isset($decoded['code']) && $decoded['code'] === 1001) {
+ return [
+ 'granted' => false,
+ 'error' => null,
+ 'needs_rebuild' => false
+ ];
+ }
+ }
+ }
+ }
+
+ // If we reach here, no clear permission status was found
+ return [
+ 'granted' => false,
+ 'error' => 'Unable to determine permission status',
+ 'needs_rebuild' => false
+ ];
+
+ } catch (\Exception $e) {
+ Log::error('Screen recording permission check failed: ' . $e->getMessage());
+
+ return [
+ 'granted' => false,
+ 'error' => 'Permission check failed: ' . $e->getMessage(),
+ 'needs_rebuild' => false
+ ];
+ }
+ }
+
+ /**
+ * Request screen recording permission by attempting to start capture
+ * This triggers the same permission dialog as "Start Call" and opens System Preferences
+ */
+ public function requestScreenRecordingPermission(): array
+ {
+ try {
+ // Use the Swift binary to trigger permission request using the same "start" command
+ $binaryPath = $this->getAudioCaptureBinaryPath();
+
+ if (!$binaryPath) {
+ return [
+ 'success' => false,
+ 'error' => 'Audio capture binary not found. Please rebuild the application.'
+ ];
+ }
+
+ // Use the same "start" command that triggers permission request
+ $process = Process::input('{"command": "start"}' . PHP_EOL)
+ ->timeout(15)
+ ->start([$binaryPath]);
+
+ // Wait for the process to trigger permission request
+ $startTime = time();
+ $timeout = 15;
+ $permissionRequested = false;
+
+ while (time() - $startTime < $timeout && $process->running()) {
+ $output = $process->latestOutput();
+
+ if (!empty($output)) {
+ $lines = explode("\n", $output);
+
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if (empty($line)) continue;
+
+ if ($decoded = json_decode($line, true)) {
+ if (isset($decoded['type'])) {
+ if ($decoded['type'] === 'status' && $decoded['state'] === 'capturing') {
+ // Permission was already granted
+ $process->signal(15); // SIGTERM
+ return [
+ 'success' => true,
+ 'error' => null,
+ 'message' => 'Permission was already granted.'
+ ];
+ } elseif ($decoded['type'] === 'error' && isset($decoded['code']) && $decoded['code'] === 1001) {
+ // Permission denied, system preferences should open
+ $permissionRequested = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if ($permissionRequested) {
+ break;
+ }
+
+ usleep(200000); // 200ms sleep
+ }
+
+ // Kill the process
+ if ($process->running()) {
+ $process->signal(15); // SIGTERM
+ }
+
+ $process->wait();
+
+ return [
+ 'success' => true,
+ 'error' => null,
+ 'message' => 'Permission request initiated. Please grant screen recording permission in System Preferences.'
+ ];
+
+ } catch (\Exception $e) {
+ Log::error('Screen recording permission request failed: ' . $e->getMessage());
+
+ return [
+ 'success' => false,
+ 'error' => 'Permission request failed: ' . $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * Get the path to the audio capture binary
+ */
+ private function getAudioCaptureBinaryPath(): ?string
+ {
+ $basePath = base_path();
+
+ // Try the build directory first
+ $buildPath = $basePath . '/' . self::AUDIO_CAPTURE_BINARY;
+ if (file_exists($buildPath) && is_executable($buildPath)) {
+ return $buildPath;
+ }
+
+ // Try the extras directory
+ $extrasPath = $basePath . '/' . self::EXTRAS_BINARY;
+ if (file_exists($extrasPath) && is_executable($extrasPath)) {
+ return $extrasPath;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get permission status with user-friendly message
+ */
+ public function getPermissionStatus(): array
+ {
+ $result = $this->checkScreenRecordingPermission();
+
+ if ($result['needs_rebuild']) {
+ return [
+ 'status' => 'needs_rebuild',
+ 'message' => 'Audio capture needs to be rebuilt. Please run the build script.',
+ 'action' => 'rebuild'
+ ];
+ }
+
+ if ($result['error']) {
+ return [
+ 'status' => 'error',
+ 'message' => $result['error'],
+ 'action' => 'retry'
+ ];
+ }
+
+ if ($result['granted']) {
+ return [
+ 'status' => 'granted',
+ 'message' => 'Screen recording permission is granted. You can start capturing audio.',
+ 'action' => 'none'
+ ];
+ }
+
+ return [
+ 'status' => 'denied',
+ 'message' => 'Screen recording permission is required for audio capture. Please grant permission in System Preferences.',
+ 'action' => 'request'
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Services/OnboardingStateService.php b/app/Services/OnboardingStateService.php
new file mode 100644
index 0000000..daeb3d6
--- /dev/null
+++ b/app/Services/OnboardingStateService.php
@@ -0,0 +1,199 @@
+= 1 && $step <= 3) {
+ Cache::forever(self::STEP_CACHE_KEY, $step);
+ Log::info("Onboarding step set to: $step");
+ }
+ }
+
+ /**
+ * Mark a specific step as completed
+ */
+ public function markStepCompleted(int $step): void
+ {
+ if ($step >= 1 && $step <= 3) {
+ $completedSteps = $this->getCompletedSteps();
+ $completedSteps[$step] = true;
+
+ Cache::forever(self::CACHE_KEY, $completedSteps);
+ Log::info("Onboarding step $step marked as completed");
+ }
+ }
+
+ /**
+ * Reset a specific step (mark as incomplete)
+ */
+ public function resetStep(int $step): void
+ {
+ if ($step >= 1 && $step <= 3) {
+ $completedSteps = $this->getCompletedSteps();
+ $completedSteps[$step] = false;
+
+ Cache::forever(self::CACHE_KEY, $completedSteps);
+ Log::info("Onboarding step $step reset to incomplete");
+ }
+ }
+
+ /**
+ * Check if a specific step is completed
+ */
+ public function isStepCompleted(int $step): bool
+ {
+ $completedSteps = $this->getCompletedSteps();
+ return $completedSteps[$step] ?? false;
+ }
+
+ /**
+ * Get all completed steps
+ */
+ public function getCompletedSteps(): array
+ {
+ return Cache::get(self::CACHE_KEY, [
+ 1 => false, // API Key
+ 2 => false, // Screen Recording Permission
+ 3 => false, // GitHub Star
+ ]);
+ }
+
+ /**
+ * Check if onboarding is completely finished
+ */
+ public function isOnboardingComplete(): bool
+ {
+ $completedSteps = $this->getCompletedSteps();
+ return $completedSteps[1] && $completedSteps[2] && $completedSteps[3];
+ }
+
+ /**
+ * Get the next incomplete step
+ */
+ public function getNextIncompleteStep(): int
+ {
+ $completedSteps = $this->getCompletedSteps();
+
+ for ($step = 1; $step <= 3; $step++) {
+ if (!($completedSteps[$step] ?? false)) {
+ return $step;
+ }
+ }
+
+ return 3; // All steps completed, return last step
+ }
+
+ /**
+ * Reset onboarding state (for testing or re-onboarding)
+ */
+ public function resetOnboarding(): void
+ {
+ Cache::forget(self::CACHE_KEY);
+ Cache::forget(self::STEP_CACHE_KEY);
+ Log::info("Onboarding state reset");
+ }
+
+ /**
+ * Get onboarding status for API responses
+ */
+ public function getOnboardingStatus(): array
+ {
+ $completedSteps = $this->getCompletedSteps();
+ $currentStep = $this->getCurrentStep();
+ $isComplete = $this->isOnboardingComplete();
+
+ return [
+ 'current_step' => $currentStep,
+ 'completed_steps' => $completedSteps,
+ 'is_complete' => $isComplete,
+ 'next_step' => $isComplete ? null : $this->getNextIncompleteStep(),
+ ];
+ }
+
+ /**
+ * Advanced to next step if current step is completed
+ */
+ public function advanceToNextStep(): int
+ {
+ $currentStep = $this->getCurrentStep();
+ $nextStep = $this->getNextIncompleteStep();
+
+ // If current step is completed, advance to next
+ if ($this->isStepCompleted($currentStep) && $nextStep > $currentStep) {
+ $this->setCurrentStep($nextStep);
+ return $nextStep;
+ }
+
+ return $currentStep;
+ }
+
+ /**
+ * Handle permission revocation scenario
+ * Called when system permissions are revoked outside the app
+ */
+ public function handlePermissionRevocation(): void
+ {
+ // Reset step 2 (permission step) to incomplete
+ $this->resetStep(2);
+
+ // Set current step to 2 so user is taken back to permission screen
+ $this->setCurrentStep(2);
+
+ Log::info("Permission revocation detected - user redirected to permission step");
+ }
+
+ /**
+ * Handle API key removal scenario
+ * Called when API key is removed outside the app
+ */
+ public function handleApiKeyRemoval(): void
+ {
+ // Reset step 1 (API key step) to incomplete
+ $this->resetStep(1);
+
+ // Also reset step 2 and 3 since they depend on step 1
+ $this->resetStep(2);
+ $this->resetStep(3);
+
+ // Set current step to 1 so user starts from beginning
+ $this->setCurrentStep(1);
+
+ Log::info("API key removal detected - user redirected to API key step");
+ }
+
+ /**
+ * Handle incomplete onboarding scenario
+ * Called when steps 1&2 are complete but step 3 is missing
+ */
+ public function handleIncompleteOnboarding(): void
+ {
+ // Ensure steps 1&2 are marked as completed
+ $this->markStepCompleted(1);
+ $this->markStepCompleted(2);
+
+ // Set current step to 3 so user is taken to GitHub star step
+ $this->setCurrentStep(3);
+
+ Log::info("Incomplete onboarding detected - user redirected to GitHub star step");
+ }
+}
\ No newline at end of file
diff --git a/build-swift-audio.sh b/build-swift-audio.sh
index a21d419..dff6917 100755
--- a/build-swift-audio.sh
+++ b/build-swift-audio.sh
@@ -23,6 +23,56 @@ if [ $? -eq 0 ]; then
echo "Executable location: build/native/macos-audio-capture"
# Make sure it's executable
chmod +x build/native/macos-audio-capture
+
+ # Attempt to sign the binary with available certificate
+ echo "🔐 Attempting to sign Swift audio capture binary..."
+
+ # Try to find a Developer ID Application certificate first (for distribution)
+ DIST_CERT=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | cut -d'"' -f2)
+
+ # If no distribution cert, try Development certificate (for local testing)
+ if [ -z "$DIST_CERT" ]; then
+ DEV_CERT=$(security find-identity -v -p codesigning | grep "Apple Development" | head -1 | cut -d'"' -f2)
+ CERT_NAME="$DEV_CERT"
+ CERT_TYPE="Development"
+ else
+ CERT_NAME="$DIST_CERT"
+ CERT_TYPE="Distribution"
+ fi
+
+ if [ -n "$CERT_NAME" ]; then
+ echo "📋 Using $CERT_TYPE certificate: $CERT_NAME"
+ codesign --force --sign "$CERT_NAME" \
+ --options runtime \
+ --entitlements native/macos-audio-capture/entitlements.plist \
+ build/native/macos-audio-capture
+
+ if [ $? -eq 0 ]; then
+ echo "✅ Swift audio capture signed successfully with $CERT_TYPE certificate"
+
+ # Also sign the extras directory binary if it exists
+ if [ -f "extras/macos-audio-capture" ]; then
+ echo "🔐 Signing extras audio capture binary..."
+ codesign --force --sign "$CERT_NAME" \
+ --options runtime \
+ --entitlements native/macos-audio-capture/entitlements.plist \
+ extras/macos-audio-capture
+
+ if [ $? -eq 0 ]; then
+ echo "✅ Extras audio capture signed successfully"
+ else
+ echo "⚠️ Warning: Failed to sign extras audio capture (continuing anyway)"
+ fi
+ fi
+ else
+ echo "⚠️ Warning: Failed to sign Swift audio capture (continuing anyway)"
+ fi
+ else
+ echo "⚠️ No code signing certificate found - creating unsigned build"
+ echo "💡 To create signed builds, install an Apple Developer certificate in Keychain"
+ echo " For distribution: 'Developer ID Application' certificate"
+ echo " For development: 'Apple Development' certificate"
+ fi
else
echo "❌ Failed to build Swift audio capture"
exit 1
diff --git a/build/native/macos-audio-capture b/build/native/macos-audio-capture
index be8d60e..1b05092 100755
Binary files a/build/native/macos-audio-capture and b/build/native/macos-audio-capture differ
diff --git a/config/nativephp.php b/config/nativephp.php
index 3d3e458..3104b91 100644
--- a/config/nativephp.php
+++ b/config/nativephp.php
@@ -13,8 +13,7 @@
* usually in the form of a reverse domain name.
* For example: com.nativephp.app
*/
- '
- ' => env('NATIVEPHP_APP_ID', 'com.nativephp.app'),
+ 'app_id' => env('NATIVEPHP_APP_ID', 'com.nativephp.app'),
/**
* If your application allows deep linking, you can specify the scheme
diff --git a/extras/macos-audio-capture b/extras/macos-audio-capture
index f4bae59..75dde4c 100755
Binary files a/extras/macos-audio-capture and b/extras/macos-audio-capture differ
diff --git a/native/macos-audio-capture/entitlements.plist b/native/macos-audio-capture/entitlements.plist
new file mode 100644
index 0000000..b2f1b0d
--- /dev/null
+++ b/native/macos-audio-capture/entitlements.plist
@@ -0,0 +1,44 @@
+
+
+
+ Clueless needs screen recording permission to capture system audio during meetings. This permission is required + for the app to work properly. +
+ +Permission granted!
+Permission required
+Checking permission...
+{{ permissionMessage }}
+Manage system permissions required for Clueless to function properly.
++ Required for system audio capture during meetings +
++ Clueless uses macOS ScreenCaptureKit to capture system audio during meetings. This requires + screen recording permission, but the app only captures audio data - no visual content is + recorded or stored. +
++ To manually grant permission: Go to System Preferences → Privacy & Security → + Screen Recording, then enable Clueless. +
+