|
| 1 | +# Mobile App Implementation Summary |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Implemented iOS and Android mobile apps using **Hotwire Native** as hybrid wrappers around the existing Symfony web app. The apps reuse 90%+ of the web codebase while having platform-specific billing (App Store for iOS, Play Store for Android, Stripe for web). |
| 6 | + |
| 7 | +## What Was Implemented |
| 8 | + |
| 9 | +### Phase 1: Platform Detection |
| 10 | +- **Platform enum** (`src/Value/Platform.php`) - Web/iOS/Android detection |
| 11 | +- **PlatformDetector service** - Detects platform from User-Agent header |
| 12 | +- **PlatformTwigExtension** - Twig functions `is_web()`, `is_ios()`, `is_android()`, `is_native_app()` |
| 13 | +- **JavaScript detection** in `app.js` - Sets `window.isNativeApp` and `window.nativePlatform` |
| 14 | +- **CSS classes** - `platform-web`, `platform-ios`, `platform-android`, `native-app` |
| 15 | + |
| 16 | +### Phase 2: Billing Infrastructure |
| 17 | +- **Database migration** - Added `platform` column to `membership` table |
| 18 | +- **Membership entity** - Added platform property and helper methods |
| 19 | +- **Billing interface** (`PlatformBillingInterface`) - Common contract for all platforms |
| 20 | +- **Platform billing services** - `WebStripeBilling`, `IosAppStoreBilling`, `AndroidPlayBilling` |
| 21 | +- **BillingFactory** - Returns appropriate service based on detected platform |
| 22 | + |
| 23 | +### Phase 3: Platform-Specific UI |
| 24 | +- **base.html.twig** - Header/footer wrapped in `{% if is_web() %}` conditionals |
| 25 | +- **membership.html.twig** - Platform-specific subscription buttons and management links |
| 26 | +- **Translations** - Added keys for all 6 languages (en, cs, de, es, fr, ja) |
| 27 | + |
| 28 | +### Phase 4: Native Scanner Bridge |
| 29 | +- **barcode_scanner_controller.js** - Added native bridge methods |
| 30 | + - `window.onNativeScanResult(code)` callback |
| 31 | + - `window.onNativeScanCancelled()` callback |
| 32 | + - Auto-detects native app and uses native scanner instead of web camera |
| 33 | + |
| 34 | +### Phase 5: API Endpoints |
| 35 | +- `POST /api/ios/verify-receipt` - iOS App Store receipt verification |
| 36 | +- `POST /api/android/verify-purchase` - Android Play Store purchase verification |
| 37 | + |
| 38 | +### Phase 6: iOS App (`ios/`) |
| 39 | +- Swift Package Manager project with Hotwire Native iOS dependency |
| 40 | +- `WebViewController` with JavaScript bridges for scanner and billing |
| 41 | +- `BarcodeScannerBridge` - Native AVFoundation barcode scanner (EAN-8/EAN-13) |
| 42 | +- `StoreKitManager` - StoreKit 2 in-app purchases |
| 43 | +- `BillingBridge` - JavaScript bridge for purchases |
| 44 | + |
| 45 | +### Phase 7: Android App (`android/`) |
| 46 | +- Gradle project with Hotwire Turbo Android dependency |
| 47 | +- `TurboWebFragment` with JavaScript interfaces |
| 48 | +- `BarcodeScannerBridge` - CameraX + ML Kit barcode scanner |
| 49 | +- `BillingManager` - Google Play Billing Library integration |
| 50 | +- Material Design UI with scan overlay |
| 51 | + |
| 52 | +### Phase 8: CI/CD |
| 53 | +- `.github/workflows/ios.yml` - Builds on macOS, runs tests |
| 54 | +- `.github/workflows/android.yml` - Builds APK, uploads artifacts |
| 55 | + |
| 56 | +## Problems Faced & Solutions |
| 57 | + |
| 58 | +### 1. PHPStan Error with `is_array()` Check |
| 59 | +**Problem:** PHPStan complained that `is_array()` always evaluates to true when a `@var` annotation was placed before `json_decode()`. |
| 60 | + |
| 61 | +**Solution:** Moved the `@var` annotation to after the `is_array()` check: |
| 62 | +```php |
| 63 | +// Before (error) |
| 64 | +/** @var array<string, mixed> $data */ |
| 65 | +$data = json_decode($content, true); |
| 66 | + |
| 67 | +// After (fixed) |
| 68 | +$data = json_decode($content, true); |
| 69 | +if (!is_array($data)) { return error; } |
| 70 | +/** @var array<string, mixed> $data */ |
| 71 | +``` |
| 72 | + |
| 73 | +### 2. Duplicate Companion Object in Kotlin |
| 74 | +**Problem:** Android `BarcodeScannerBridge.kt` had two `companion object` declarations. |
| 75 | + |
| 76 | +**Solution:** Consolidated all constants into a single companion object at the bottom of the class. |
| 77 | + |
| 78 | +### 3. Turbo Globally Disabled |
| 79 | +**Problem:** Turbo is disabled via `data-turbo="false"` on `<html>` element. |
| 80 | + |
| 81 | +**Solution:** This is intentional per project architecture. Native apps use Hotwire Native which handles navigation natively, so Turbo being disabled doesn't affect mobile apps. |
| 82 | + |
| 83 | +## File Structure |
| 84 | + |
| 85 | +``` |
| 86 | +ios/ |
| 87 | +├── Package.swift |
| 88 | +├── .gitignore |
| 89 | +└── MySpeedPuzzling/ |
| 90 | + ├── App/MySpeedPuzzlingApp.swift |
| 91 | + ├── Navigation/MainNavigationView.swift |
| 92 | + ├── Web/WebViewController.swift |
| 93 | + ├── Features/BarcodeScannerBridge.swift |
| 94 | + └── Billing/ |
| 95 | + ├── StoreKitManager.swift |
| 96 | + └── BillingBridge.swift |
| 97 | +
|
| 98 | +android/ |
| 99 | +├── build.gradle.kts |
| 100 | +├── settings.gradle.kts |
| 101 | +├── gradlew |
| 102 | +└── app/ |
| 103 | + └── src/main/ |
| 104 | + ├── AndroidManifest.xml |
| 105 | + ├── java/com/myspeedpuzzling/ |
| 106 | + │ ├── app/MainActivity.kt |
| 107 | + │ ├── app/TurboWebFragment.kt |
| 108 | + │ ├── features/BarcodeScannerBridge.kt |
| 109 | + │ ├── features/BarcodeScannerActivity.kt |
| 110 | + │ └── billing/ |
| 111 | + │ ├── BillingBridge.kt |
| 112 | + │ └── BillingManager.kt |
| 113 | + └── res/ |
| 114 | +``` |
| 115 | + |
| 116 | +## Next Steps |
| 117 | + |
| 118 | +### Immediate (Required for Launch) |
| 119 | +1. **Run migration:** `docker compose exec web php bin/console doctrine:migrations:migrate` |
| 120 | +2. **Implement actual receipt verification** in `IosAppStoreBilling` and `AndroidPlayBilling` (currently stubs) |
| 121 | +3. **Set up App Store Connect:** Create app, configure in-app purchase products |
| 122 | +4. **Set up Google Play Console:** Create app, configure subscription products |
| 123 | + |
| 124 | +### For Production Deployment |
| 125 | +5. **Code signing:** |
| 126 | + - iOS: Add certificates and provisioning profiles to GitHub secrets |
| 127 | + - Android: Create and secure signing keystore |
| 128 | +6. **App icons and launch screens** for both platforms |
| 129 | +7. **Webhook endpoints** for subscription status updates from Apple/Google |
| 130 | +8. **TestFlight/Internal testing** before public release |
| 131 | + |
| 132 | +### Product IDs |
| 133 | +- **iOS:** `com.myspeedpuzzling.premium.monthly`, `com.myspeedpuzzling.premium.yearly` |
| 134 | +- **Android:** `premium_monthly`, `premium_yearly` |
| 135 | + |
| 136 | +## Testing |
| 137 | + |
| 138 | +The web app continues to work normally. All PHP checks pass: |
| 139 | +- PHPStan: ✅ |
| 140 | +- PHPCS: ✅ |
| 141 | +- PHPUnit: ✅ |
| 142 | +- Schema validation: ✅ |
| 143 | +- Cache warmup: ✅ |
| 144 | + |
| 145 | +Native apps are isolated in `ios/` and `android/` directories and cannot break the web app. |
0 commit comments