After successful login on web or desktop, users are prompted to install the native mobile app. The prompt displays a QR code for easy scanning and a direct download link to the latest APK release from GitHub.
- Shows only to authenticated users on web/desktop
- Automatically fetches latest APK from GitHub
- Displays QR code for instant setup
- Provides direct download link
- Includes release notes and version info
- Respects user dismissal (24-hour cooldown)
- Real-time generation from latest APK download URL
- Uses free QR code API (qrserver.com)
- Direct mobile browser scanning support
- No additional dependencies needed
- Automatically fetches latest release
- Extracts APK filename and size
- Caches release info (configurable)
- Handles API rate limiting gracefully
- Falls back gracefully if GitHub is unavailable
- Detects if app already installed
- Skips prompt for native app users
- Detects PWA installation
- Detects Capacitor/Cordova environments
Main hook for managing mobile install prompts.
import { useMobileInstallPrompt } from '@/hooks/use-mobile-install-prompt';
export function MyPage() {
const { user } = useAuth();
const {
show, // Display the prompt?
loading, // Currently fetching?
error, // Error message
releaseInfo, // GitHub release data
dismissPrompt, // Dismiss function
installDetected, // App already installed?
} = useMobileInstallPrompt(!!user);
if (!show) return null;
return (
<div>
{loading && <LoadingSpinner />}
{error && <ErrorMessage message={error} />}
{releaseInfo && (
<>
<img src={releaseInfo.qrCodeUrl} alt="Download QR" />
<a href={releaseInfo.downloadUrl}>Download APK</a>
<button onClick={dismissPrompt}>Not Now</button>
</>
)}
</div>
);
}Parameters:
isAuthenticated(boolean): User is logged in
Returns:
{
show: boolean; // Show prompt?
loading: boolean; // Loading release info?
error: string | null; // Error message
releaseInfo: MobileReleaseInfo | null; // GitHub release data
installDetected: boolean; // App already installed?
dismissPrompt: () => void; // Hide prompt
resetPrompt: () => void; // Reset for testing
}Tracks app installation state.
import { useInstallationState } from '@/hooks/use-mobile-install-prompt';
export function App() {
const { isInstalled } = useInstallationState();
return (
<>
{isInstalled ? <NativeApp /> : <WebApp />}
</>
);
}Manually generate QR code URLs.
import { generateQRCode } from "@/hooks/use-mobile-install-prompt";
const qrUrl = generateQRCode("https://example.com/app.apk");
// Returns: https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=...Complete modal component, ready to use.
import { MobileInstallPrompt } from '@/components/MobileInstallPrompt';
export function RootLayout() {
return (
<>
<Header />
<Main />
<MobileInstallPrompt />
</>
);
}Features:
- Automatic modal rendering
- QR code display
- Direct download button
- Copy-link button
- Version and size info
- Release notes expandable section
- Responsive design
- Dark mode support
The component is already integrated into the root layout. No additional setup needed!
✅ src/components/MobileInstallPrompt.tsx - Modal component
✅ src/hooks/use-mobile-install-prompt.ts - Hook and utilities
✅ src/routes/__root.tsx - Already integrated
The component is automatically rendered in your root layout and shows when:
- User is authenticated
- Not dismissed in last 24 hours
- App not already installed
- Running on web/desktop (not native)
For advanced use cases:
import { useMobileInstallPrompt } from '@/hooks/use-mobile-install-prompt';
export function CustomInstallPrompt() {
const { user } = useAuth();
const {
show,
loading,
releaseInfo,
dismissPrompt,
} = useMobileInstallPrompt(!!user);
if (!show || !releaseInfo) return null;
return (
<div className="custom-modal">
<h2>Get the app on your phone</h2>
{loading ? (
<Spinner />
) : (
<>
<img
src={releaseInfo.qrCodeUrl}
alt="Scan to download"
className="qr-code"
/>
<a
href={releaseInfo.downloadUrl}
className="btn-primary"
download
>
Download v{releaseInfo.version}
</a>
<p className="text-sm text-gray-500">
Size: {releaseInfo.apkSize}
</p>
<button
onClick={dismissPrompt}
className="btn-secondary"
>
Maybe later
</button>
</>
)}
</div>
);
}import { useMobileInstallPrompt } from '@/hooks/use-mobile-install-prompt';
export function TestComponent() {
const prompt = useMobileInstallPrompt(true);
// Force show prompt for testing
const handleTest = () => {
prompt.resetPrompt();
};
return (
<div>
<button onClick={handleTest}>Test Install Prompt</button>
{prompt.show && <MobileInstallPrompt />}
</div>
);
}User Logs In
↓
useMobileInstallPrompt() initializes
↓
Check if dismissed (24h cooldown)
↓
Check if already installed
↓
Fetch latest GitHub release (cached)
↓
Generate QR code URL
↓
Display modal
↓
User scans QR or clicks download
↓
APK downloads
↓
User installs app
↓
Next login: Native app runs instead
-
Hook fetches from GitHub API:
GET https://api.github.com/repos/servicespash/lattyscymatichub/releases/latest -
Extracts APK asset:
{ "name": "app-release.apk", "browser_download_url": "https://github.com/.../releases/download/v1.0.0/app.apk", "size": 52428800 } -
Generates QR code:
https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=<encoded-apk-url>
- GitHub repository:
servicespash/lattyscymatichub - Latest release must have an
.apkasset - Public repository access
To use a different repository:
// In src/hooks/use-mobile-install-prompt.ts
// Update the fetch URL:
const response = await fetch("https://api.github.com/repos/YOUR_ORG/YOUR_REPO/releases/latest");// Dismissed flag (24h cooldown)
"mobile_install_prompt_dismissed";
// Last check timestamp
"mobile_last_install_check";// Clear dismissal
localStorage.removeItem("mobile_install_prompt_dismissed");
localStorage.removeItem("mobile_last_install_check");
// Or use the hook
const prompt = useMobileInstallPrompt(true);
prompt.resetPrompt();// In src/hooks/use-mobile-install-prompt.ts
const dismissalWindow = 24 * 60 * 60 * 1000; // 24 hours// In src/hooks/use-mobile-install-prompt.ts
const INSTALL_CHECK_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 7 days// In src/hooks/use-mobile-install-prompt.ts
const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodedUrl}`;Edit src/components/MobileInstallPrompt.tsx styles directly.
If GitHub is down, the prompt gracefully fails:
- Error message displayed: "Failed to fetch latest release"
- Prompt will retry on next login
- User can manually visit GitHub releases
If latest release has no APK:
- Warning logged to console
- Prompt doesn't show
- User can download from GitHub manually
- Retry on next session
- Dismissal still honored
- Error logged for debugging
- Release info cached per session
- QR code URL generated client-side
- No heavy dependencies
- ~2KB additional bundle size
- API calls cached by browser
First login: Fetch from GitHub (5 requests)
Same session: Use cached release (0 requests)
After 7 days: Fetch again
After dismiss: Wait 24 hours for next prompt
- Web login → Installation prompt shows
- Scans QR → Mobile browser downloads APK
- Installation begins → System prompts for install
- App installed → Next login uses native app
- Auto-updates → Via GitHub Actions workflow
- Desktop login → Installation prompt shows
- Clicks download → APK saved to downloads
- Transfer to phone → Manual transfer via USB/cloud
- Installation → User opens APK on phone
- App installed → Same as Android flow
// Add to MobileInstallPrompt component
useEffect(() => {
if (show) {
analytics.track("mobile_install_prompt_shown", {
version: releaseInfo?.version,
timestamp: new Date(),
});
}
}, [show]);
// Track downloads
const handleDownload = () => {
analytics.track("mobile_app_downloaded", {
version: releaseInfo?.version,
});
};- Always provide fallback for download failures
- Test QR codes before release
- Monitor GitHub API usage
- Include release notes in GitHub releases
- Sign APK properly for production
- Test prompt flow on web and desktop
- Track download metrics for engagement
- Update GitHub release before web updates
Check:
- GitHub API is accessible
- Latest release has APK asset
- Browser allows image from qrserver.com
Solution:
// Add error boundary
if (!releaseInfo?.qrCodeUrl) {
return <fallbackUI />;
}Check:
- User is authenticated
- Not dismissed in last 24 hours
- Not running native app
- App not already installed
Test:
const prompt = useMobileInstallPrompt(true);
prompt.resetPrompt(); // Force showCheck:
- GitHub release still exists
- APK file still available
- Browser allows downloads
Fallback:
https://github.com/servicespash/lattyscymatichub/releases/latest
For issues or questions:
- Check console for error messages
- Verify GitHub release exists
- Test QR code generation
- Check network requests in DevTools
src/components/MobileInstallPrompt.tsx- Componentsrc/hooks/use-mobile-install-prompt.ts- Hook implementationsrc/routes/__root.tsx- Integration point.github/workflows/build-apk.yml- APK build workflow