diff --git a/docs/android-capacitor-oauth-template.md b/docs/android-capacitor-oauth-template.md index 642a161..5df849c 100644 --- a/docs/android-capacitor-oauth-template.md +++ b/docs/android-capacitor-oauth-template.md @@ -229,7 +229,111 @@ --- -## 6. Чеклист для следующего проекта +## 6. Онлайн-загрузка SPA (тонкий клиент) + +> **Зачем:** не пересобирать APK при каждом изменении UI. SPA грузится с сервера — обновляешь `dist/` на сервере, приложение подхватывает автоматически. + +### Архитектура + +``` +┌─────────────────┐ ┌──────────────────┐ +│ Android App │────▶│ Server SPA │ +│ (Capacitor WV) │ │ (dist/index.html│ +│ тонкий клиент │ │ + JS + CSS) │ +└─────────────────┘ └──────────────────┘ + │ │ + │ ▼ + │ ┌──────────────────┐ + └────────────│ PHP API │ + XHR │ /api/v1/* │ + └──────────────────┘ +``` + +### Как это работает + +**Первый запуск** (нет `server_url` в Preferences): +1. Загружается локальный `index.html` из APK (`assets/public`) +2. Vue router показывает `/mobile-setup` +3. Пользователь вводит адрес сервера +4. URL сохраняется через `@capacitor/preferences` → `SharedPreferences("CapacitorStorage")` +5. `App.exitApp()` — приложение закрывается + +**Второй и последующие запуски**: +1. `MainActivity.onCreate()` **перед** `super.onCreate()` читает `server_url` из Preferences +2. Если URL есть — создаёт `CapConfig` через `Builder` с `setServerUrl(url)` +3. `super.onCreate(savedInstanceState)` видит этот `config` и WebView грузит SPA с сервера +4. Все API-запросы идут на тот же origin — **CORS не нужен** + +### MainActivity.java + +```java +public class MainActivity extends BridgeActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(R.style.AppTheme_NoActionBar); + + // ⚠️ ДО super.onCreate() — иначе BridgeActivity загрузит bundled assets + SharedPreferences prefs = getSharedPreferences("CapacitorStorage", MODE_PRIVATE); + String serverUrl = prefs.getString("server_url", null); + + if (serverUrl != null && !serverUrl.isEmpty()) { + this.config = new CapConfig.Builder(this) + .setServerUrl(serverUrl) + .create(); + } + + super.onCreate(savedInstanceState); + // ... rest of onCreate (status bar, edge-to-edge, etc.) + } +} +``` + +**Критически важно:** `this.config` присваивается **перед** `super.onCreate()`. `BridgeActivity.onCreate()` вызывает `load()` → `bridgeBuilder.setConfig(config)` → если `config` уже есть, `CapConfig` читает `server.url` и WebView открывает его вместо `file:///android_asset/public/index.html`. + +### MobileSetupPage.vue + +```vue + +``` + +### Почему не `capacitor.config.json`? + +В `capacitor.config.json` можно статически прописать `"server": { "url": "https://..." }`, но тогда **всегда** будет грузиться с сервера, даже при первом запуске (когда URL ещё неизвестен). Динамический `CapConfig.Builder` в `MainActivity` позволяет: +- Первый запуск → bundled assets → `/mobile-setup` +- Последующие → remote SPA + +### Что остаётся в APK + +| Что | Зачем | +|-----|-------| +| Bundled `dist/` в `assets/public` | Fallback для первого запуска и офлайн-режима | +| Capacitor plugins (App, Preferences, StatusBar) | Нативная функциональность | +| Splash screen, themes, layouts | Нативная обёртка | +| Deep link intent-filter | OAuth callback | + +### Важные нюансы + +- **`usesCleartextTraffic="true"`** в `AndroidManifest.xml` — нужен для HTTP (локальная сеть) +- **Splash screen** работает одинаково — до загрузки SPA показывается нативный splash +- **Status bar / edge-to-edge** — не затрагивается, фиксы из раздела 4 работают +- **Deep link `shserv://auth/callback`** — обрабатывается на уровне Android (intent-filter), не зависит от `serverUrl` +- **Ошибка соединения** — если сервер недоступен, WebView покажет стандартную ошибку загрузки страницы. Можно добавить `onReceivedError` в WebViewClient для graceful fallback. + +--- + +## 8. Чеклист для следующего проекта ### Capacitor setup - [ ] `capacitor.config.json` с `"appId": "com.company.app"` — уникальный @@ -249,6 +353,12 @@ - [ ] Storage abstraction (`storage.js`) — единая точка для Preferences/localStorage - [ ] Navigation abstraction (`navigation.js`) — единая точка для OAuth redirect +### Онлайн-загрузка SPA (тонкий клиент) +- [ ] `MainActivity.onCreate()` читает `server_url` из Preferences **перед** `super.onCreate()` +- [ ] `CapConfig.Builder(this).setServerUrl(url)` если URL сохранён +- [ ] `/mobile-setup` страница — сохраняет URL и вызывает `App.exitApp()` +- [ ] Bundled `dist/` в APK как fallback для первого запуска + ### Android visual - [ ] Custom `MainActivity.java` с `setTheme()` ПЕРЕД `super.onCreate()` - [ ] Custom `capacitor_bridge_layout_main.xml` с `fitsSystemWindows="true"` @@ -265,7 +375,7 @@ --- -## 7. Ключевые файлы проекта +## 9. Ключевые файлы проекта | Файл | Зачем | |------|-------| @@ -274,6 +384,8 @@ | `webclient/src/api/server-config.js` | `isNativeApp()`, `buildServerUrl()`, storage | | `webclient/src/api/storage.js` | Единый адаптер Preferences/localStorage | | `webclient/src/api/navigation.js` | `redirectToOAuth()`, `getOAuthReturnTo()` | +| `webclient/src/features/auth/pages/MobileSetupPage.vue` | Страница ввода адреса сервера (только native) | +| `webclient/src/api/storage.js` | Единый адаптер Preferences/localStorage | | `webclient/capacitor.config.json` | Capacitor config, plugins, android scheme | | `android/app/src/main/java/.../MainActivity.java` | Theme override, fitsSystemWindows, edge-to-edge opt-out | | `android/app/src/main/res/layout/capacitor_bridge_layout_main.xml` | Override с fitsSystemWindows="true" |