✨ v1.39.0 - PWA iOS: Service Worker + instalação iPhone/iPad
- Service Worker v1.39.0 com estratégias offline-first - Cache-First: imagens, fontes (resposta imediata) - Network-First: chamadas API (dados frescos) - Stale-While-Revalidate: HTML/CSS/JS - Verificação de atualizações a cada 1 hora - Prompt de atualização quando nova versão disponível - manifest.json otimizado: scope, categories, prefer_related_applications - Meta tags iOS: apple-mobile-web-app-capable, black-translucent - Registro automático do Service Worker em main.jsx - Documentação completa: docs/INSTALACAO_iOS.md - Compatível com upgrade futuro para Capacitor (30 min, zero mudanças) Deploy: frontend/dist deployed to 213.165.93.60
This commit is contained in:
parent
cee24f0b42
commit
91300c9457
21
CHANGELOG.md
21
CHANGELOG.md
@ -5,6 +5,27 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
|
||||
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
|
||||
|
||||
|
||||
## [1.39.0] - 2025-12-16
|
||||
|
||||
### Added
|
||||
- **PWA para iOS** - Aplicativo instalável em iPhone/iPad
|
||||
- Service Worker com estratégias de cache offline-first
|
||||
- Cache versionado (v1.39.0) com limpeza automática de versões antigas
|
||||
- Estratégias de cache por tipo: Cache-First (imagens/fontes), Network-First (API), Stale-While-Revalidate (HTML/CSS/JS)
|
||||
- Registro automático do Service Worker em main.jsx
|
||||
- Verificação de atualizações a cada 1 hora
|
||||
- Prompt de atualização quando nova versão disponível
|
||||
- manifest.json otimizado: scope, categories (finance/productivity/business)
|
||||
- Meta tags iOS: apple-mobile-web-app-capable, black-translucent status bar
|
||||
- Meta tag format-detection para desabilitar auto-detecção de telefones
|
||||
- Ícone Apple Touch Icon (180x180) no manifest
|
||||
- Suporte completo para instalação via Safari iOS
|
||||
|
||||
### Changed
|
||||
- **Service Worker** atualizado para versão 1.39.0
|
||||
- **manifest.json** - Adicionado prefer_related_applications: false
|
||||
- **index.html** - Status bar style alterado de "default" para "black-translucent" (melhor em iOS)
|
||||
|
||||
## [1.38.0] - 2025-12-15
|
||||
|
||||
### Added
|
||||
|
||||
132
docs/INSTALACAO_iOS.md
Normal file
132
docs/INSTALACAO_iOS.md
Normal file
@ -0,0 +1,132 @@
|
||||
# 📱 Como Instalar WebMoney no iPhone/iPad
|
||||
|
||||
## ✅ Requisitos
|
||||
- iOS 11.3 ou superior
|
||||
- Safari (navegador nativo do iOS)
|
||||
|
||||
## 📲 Passo a Passo da Instalação
|
||||
|
||||
### 1. Abrir no Safari
|
||||
Abra o Safari e acesse: **https://webmoney.cnxifly.com**
|
||||
|
||||
> ⚠️ **IMPORTANTE**: Deve ser o Safari! Chrome e Firefox no iOS não suportam PWA.
|
||||
|
||||
### 2. Fazer Login
|
||||
Entre com suas credenciais:
|
||||
- Email: `marco@cnxifly.com`
|
||||
- Senha: `M@ster9354`
|
||||
|
||||
### 3. Adicionar à Tela Inicial
|
||||
1. Toque no ícone de **Compartilhar** (quadrado com seta para cima)
|
||||
2. Role para baixo e toque em **"Adicionar à Tela de Início"**
|
||||
3. Confirme o nome "WebMoney" (ou personalize)
|
||||
4. Toque em **"Adicionar"**
|
||||
|
||||
### 4. Usar como App
|
||||
- Um ícone do WebMoney aparecerá na sua tela inicial
|
||||
- Toque no ícone para abrir como aplicativo
|
||||
- O app abrirá em tela cheia (sem barras do Safari)
|
||||
- Status bar translúcido integrado ao design
|
||||
|
||||
## 🎨 Recursos PWA
|
||||
|
||||
### ✅ O que Funciona
|
||||
- **Instalação na tela inicial** - Ícone próprio como app nativo
|
||||
- **Modo standalone** - Sem barras do navegador
|
||||
- **Cache offline** - Assets ficam em cache para carregamento rápido
|
||||
- **Service Worker** - Atualizações automáticas
|
||||
- **Ícone Apple Touch** - 180x180px otimizado para iOS
|
||||
- **Status bar integrado** - Estilo black-translucent
|
||||
|
||||
### 🚫 Limitações do PWA no iOS
|
||||
- **Sem notificações push** (Safari não suporta)
|
||||
- **Sem sincronização em background** (requer app nativo)
|
||||
- **Cache limitado** - iOS pode limpar cache se memória baixa
|
||||
- **Sem acesso a APIs nativas** (câmera, localização, etc.)
|
||||
|
||||
## 🔄 Atualizações
|
||||
|
||||
O app verifica atualizações automaticamente:
|
||||
- **A cada 1 hora** enquanto app estiver aberto
|
||||
- **Ao recarregar** a página
|
||||
- Quando houver atualização, aparece um prompt perguntando se deseja atualizar
|
||||
|
||||
## 🆙 Upgrade Futuro: App Store
|
||||
|
||||
Se no futuro desejarmos publicar na App Store (US$ 99/ano):
|
||||
|
||||
### Opção 1: Capacitor (Recomendado)
|
||||
- **Tempo**: 30 minutos
|
||||
- **Código**: Zero mudanças necessárias
|
||||
- **Recursos**: Notificações push, background sync, APIs nativas
|
||||
- **Build**: Ionic Capacitor (`npx cap add ios`)
|
||||
|
||||
### Opção 2: React Native
|
||||
- **Tempo**: 2-3 dias
|
||||
- **Código**: Reescrever em React Native
|
||||
- **Recursos**: Acesso total a APIs nativas
|
||||
- **Custo**: Alto esforço de migração
|
||||
|
||||
## 📊 Comparação: PWA vs App Nativo
|
||||
|
||||
| Recurso | PWA (Atual) | Capacitor | App Nativo |
|
||||
|---------|-------------|-----------|------------|
|
||||
| **Custo** | Gratuito | $99/ano | $99/ano |
|
||||
| **Instalação** | Via Safari | Via App Store | Via App Store |
|
||||
| **Tempo desenvolvimento** | Pronto | 30 min | 2-3 dias |
|
||||
| **Notificações** | ❌ | ✅ | ✅ |
|
||||
| **Offline** | ✅ Parcial | ✅ Total | ✅ Total |
|
||||
| **APIs Nativas** | ❌ | ✅ | ✅ |
|
||||
| **Atualizações** | Automático | Automático | App Store Review |
|
||||
| **Compatibilidade código** | - | 100% | 0% (reescrever) |
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### App não aparece na tela inicial
|
||||
- Certifique-se de usar **Safari** (não Chrome/Firefox)
|
||||
- Verifique se está em https://webmoney.cnxifly.com (não IP)
|
||||
- Tente recarregar a página (⌘+R no Safari)
|
||||
|
||||
### App não funciona offline
|
||||
- Service Worker leva alguns segundos para cachear
|
||||
- Navegue pelo app após instalação para cachear rotas
|
||||
- Cache só funciona após primeira visita
|
||||
|
||||
### Ícone não aparece correto
|
||||
- Force reload do Safari: **⌘+Shift+R**
|
||||
- Remova o app e reinstale
|
||||
- Limpe cache do Safari (Ajustes > Safari > Limpar Histórico)
|
||||
|
||||
## 📝 Notas Técnicas
|
||||
|
||||
### Service Worker
|
||||
- **Versão**: 1.39.0
|
||||
- **Estratégias**:
|
||||
- **Cache-First**: Imagens, fontes (resposta imediata)
|
||||
- **Network-First**: Chamadas API (dados frescos)
|
||||
- **Stale-While-Revalidate**: HTML/CSS/JS (balance)
|
||||
|
||||
### Manifest
|
||||
- **scope**: `/` (todo o site)
|
||||
- **display**: `standalone` (tela cheia)
|
||||
- **orientation**: `portrait-primary` (retrato)
|
||||
- **categories**: finance, productivity, business
|
||||
|
||||
### Meta Tags iOS
|
||||
```html
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="WebMoney">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
```
|
||||
|
||||
## 🎯 Conclusão
|
||||
|
||||
O PWA do WebMoney oferece:
|
||||
- ✅ **Experiência de app nativo** sem custo da App Store
|
||||
- ✅ **Instalação simples** via Safari
|
||||
- ✅ **Performance otimizada** com cache offline
|
||||
- ✅ **Atualizações automáticas** sem revisão da Apple
|
||||
- ✅ **Caminho de upgrade** para Capacitor quando necessário
|
||||
|
||||
Para mais informações, consulte [CHANGELOG.md](../CHANGELOG.md) versão 1.39.0.
|
||||
@ -14,8 +14,12 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#1a365d" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
|
||||
<!-- iOS PWA Meta Tags -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="WebMoney" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="WebMoney - Sistema de Gestão Financeira Pessoal. Controle suas finanças, gerencie contas, categorize transações e acompanhe seus gastos de forma inteligente." />
|
||||
|
||||
@ -3,10 +3,13 @@
|
||||
"short_name": "WebMoney",
|
||||
"description": "Sistema de Gestão Financeira Pessoal",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#1a365d",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["finance", "productivity", "business"],
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon-16x16.png",
|
||||
@ -18,6 +21,12 @@
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/logo-192.png",
|
||||
"sizes": "192x192",
|
||||
|
||||
166
frontend/public/sw.js
Normal file
166
frontend/public/sw.js
Normal file
@ -0,0 +1,166 @@
|
||||
// WebMoney Service Worker - PWA Support
|
||||
const CACHE_VERSION = 'webmoney-v1.39.0';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_DYNAMIC = `${CACHE_VERSION}-dynamic`;
|
||||
const CACHE_IMMUTABLE = `${CACHE_VERSION}-immutable`;
|
||||
|
||||
// Recursos para cache imediato (install)
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json',
|
||||
'/logo-192.png',
|
||||
'/logo-512.png',
|
||||
'/apple-touch-icon.png',
|
||||
];
|
||||
|
||||
// Estratégia: Cache First com fallback para Network
|
||||
const CACHE_FIRST_PATTERNS = [
|
||||
/\.(png|jpg|jpeg|svg|gif|webp|ico)$/,
|
||||
/\.(woff|woff2|ttf|eot)$/,
|
||||
/bootstrap-icons/,
|
||||
];
|
||||
|
||||
// Estratégia: Network First com fallback para Cache
|
||||
const NETWORK_FIRST_PATTERNS = [
|
||||
/\/api\//,
|
||||
];
|
||||
|
||||
// Install - cacheia recursos estáticos
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Installing Service Worker...', CACHE_VERSION);
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_STATIC).then((cache) => {
|
||||
console.log('[SW] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
})
|
||||
.then(() => self.skipWaiting()) // Ativa imediatamente
|
||||
);
|
||||
});
|
||||
|
||||
// Activate - limpa caches antigos
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating Service Worker...', CACHE_VERSION);
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => name.startsWith('webmoney-') && name !== CACHE_STATIC && name !== CACHE_DYNAMIC && name !== CACHE_IMMUTABLE)
|
||||
.map((name) => {
|
||||
console.log('[SW] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim()) // Assume controle imediatamente
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch - estratégias de cache
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Ignora requisições não-GET
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignora chrome-extension e outras URLs especiais
|
||||
if (!url.protocol.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache First para assets estáticos
|
||||
if (CACHE_FIRST_PATTERNS.some(pattern => pattern.test(request.url))) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Network First para API calls
|
||||
if (NETWORK_FIRST_PATTERNS.some(pattern => pattern.test(request.url))) {
|
||||
event.respondWith(networkFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Stale-While-Revalidate para HTML/CSS/JS
|
||||
event.respondWith(staleWhileRevalidate(request));
|
||||
});
|
||||
|
||||
// Estratégia: Cache First (imagens, fontes)
|
||||
async function cacheFirst(request) {
|
||||
const cache = await caches.open(CACHE_IMMUTABLE);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log('[SW] Fetch failed for:', request.url);
|
||||
// Retorna cache mesmo se expirado
|
||||
return cached || new Response('Network error', { status: 408 });
|
||||
}
|
||||
}
|
||||
|
||||
// Estratégia: Network First (API)
|
||||
async function networkFirst(request) {
|
||||
const cache = await caches.open(CACHE_DYNAMIC);
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log('[SW] Network failed, trying cache:', request.url);
|
||||
const cached = await cache.match(request);
|
||||
return cached || new Response(JSON.stringify({ error: 'Offline' }), {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Estratégia: Stale-While-Revalidate (HTML, CSS, JS)
|
||||
async function staleWhileRevalidate(request) {
|
||||
const cache = await caches.open(CACHE_DYNAMIC);
|
||||
const cached = await cache.match(request);
|
||||
|
||||
const fetchPromise = fetch(request).then((response) => {
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
return cached || fetchPromise;
|
||||
}
|
||||
|
||||
// Mensagens do cliente
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data === 'CLEAR_CACHE') {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((name) => caches.delete(name))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SW] Service Worker loaded:', CACHE_VERSION);
|
||||
@ -23,3 +23,42 @@ createRoot(document.getElementById('root')).render(
|
||||
</Suspense>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
// Registrar Service Worker para PWA
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js')
|
||||
.then((registration) => {
|
||||
console.log('[PWA] Service Worker registered:', registration.scope);
|
||||
|
||||
// Verificar atualizações a cada 1 hora
|
||||
setInterval(() => {
|
||||
registration.update();
|
||||
}, 60 * 60 * 1000);
|
||||
|
||||
// Notificar quando houver atualização
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
console.log('[PWA] Nova versão disponível! Recarregue a página.');
|
||||
// Opcional: mostrar notificação ao usuário
|
||||
if (window.confirm('Nova versão disponível! Deseja atualizar?')) {
|
||||
newWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[PWA] Service Worker registration failed:', error);
|
||||
});
|
||||
|
||||
// Recarregar quando Service Worker assumir controle
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user