Ship Android app by wrapping CRA build with Capacitor (#20)
Add Capacitor 7 scaffolding (config, android/, npm scripts, .env.mobile), native back-button/safe-area chrome, icons/splash, and signing docs so llm-fe ships one codebase to web and Android.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import './llm-fe/platform/nativeSafeArea.css';
|
||||
import App from './App';
|
||||
import AppRouter from './llm-fe/platform/AppRouter';
|
||||
import { initNativeShell } from './llm-fe/platform/initNativeShell';
|
||||
import { AuthProvider } from './llm-fe/contexts/AuthContext';
|
||||
import { AccountProvider } from './llm-fe/contexts/AccountContext';
|
||||
import { WebSocketProvider } from './llm-fe/contexts/WebSocketContext';
|
||||
@@ -11,6 +13,8 @@ import { ConversationProvider } from './llm-fe/contexts/ConversationContext';
|
||||
import { MessageProvider } from './llm-fe/contexts/MessageContext';
|
||||
import { PreferenceProvider } from './llm-fe/contexts/PreferencesContext';
|
||||
|
||||
void initNativeShell();
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Boot native Capacitor chrome (back button, status bar, keyboard).
|
||||
* Safe to call from index.tsx; no-ops when not running in a Capacitor shell.
|
||||
*/
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
import { setupAndroidBackButton, setupNativeChrome } from './nativeChrome';
|
||||
|
||||
// Side-effect import registers Preferences on window.Capacitor.Plugins for tokenStorage.
|
||||
void Preferences;
|
||||
|
||||
export async function initNativeShell() {
|
||||
await setupNativeChrome();
|
||||
return setupAndroidBackButton();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Android hardware back button + keyboard / status-bar insets for Capacitor (#20).
|
||||
* No-ops on web.
|
||||
*/
|
||||
|
||||
import { isNativePlatform } from './nativePlatform';
|
||||
|
||||
/**
|
||||
* Register Android back-button: pop history when possible, else exit app at root.
|
||||
* Call once after React mounts.
|
||||
* @returns {Promise<(() => void) | undefined>} unsubscribe
|
||||
*/
|
||||
export async function setupAndroidBackButton() {
|
||||
if (!isNativePlatform()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { App } = await import('@capacitor/app');
|
||||
|
||||
const handle = await App.addListener('backButton', ({ canGoBack }) => {
|
||||
if (canGoBack || (typeof window !== 'undefined' && window.history.length > 1)) {
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
App.exitApp();
|
||||
});
|
||||
|
||||
return () => {
|
||||
handle.remove();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure status bar + keyboard resize so MUI layout respects safe areas.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function setupNativeChrome() {
|
||||
if (!isNativePlatform()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { StatusBar, Style } = await import('@capacitor/status-bar');
|
||||
await StatusBar.setOverlaysWebView({ overlay: false });
|
||||
await StatusBar.setStyle({ style: Style.Dark });
|
||||
} catch {
|
||||
/* plugin optional at runtime */
|
||||
}
|
||||
|
||||
try {
|
||||
const { Keyboard, KeyboardResize } = await import('@capacitor/keyboard');
|
||||
if (typeof Keyboard.setResizeMode === 'function') {
|
||||
await Keyboard.setResizeMode({ mode: KeyboardResize.Body });
|
||||
}
|
||||
} catch {
|
||||
/* Keyboard.setResizeMode is Android-only; ignore on iOS / missing plugin */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { setupAndroidBackButton, setupNativeChrome } from './nativeChrome';
|
||||
|
||||
describe('nativeChrome', () => {
|
||||
const originalCapacitor = window.Capacitor;
|
||||
|
||||
afterEach(() => {
|
||||
window.Capacitor = originalCapacitor;
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('setupAndroidBackButton is no-op without Capacitor', async () => {
|
||||
delete window.Capacitor;
|
||||
const unsub = await setupAndroidBackButton();
|
||||
expect(unsub).toBeUndefined();
|
||||
});
|
||||
|
||||
test('setupNativeChrome is no-op without Capacitor', async () => {
|
||||
delete window.Capacitor;
|
||||
await expect(setupNativeChrome()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Capacitor / notch safe-area insets (#20). Harmless on desktop web. */
|
||||
html {
|
||||
padding: env(safe-area-inset-top, 0) env(safe-area-inset-right, 0)
|
||||
env(safe-area-inset-bottom, 0) env(safe-area-inset-left, 0);
|
||||
}
|
||||
|
||||
body {
|
||||
/* Keyboard plugin resizes body; keep root scrollable within inset. */
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
Reference in New Issue
Block a user