DOCUMENTAÇÃO PARA DESENVOLVEDORES
Ponte JavaScript: recursos nativos a partir do seu site
Atualizado em 4 de novembro de 2026
Prompt para Lovable, Bolt ou Base44
Adicione ao meu projeto o suporte à ponte de app do SaasToStore.
Contexto: meu site também roda dentro de um app móvel nativo criado com o SaasToStore. Dentro desse app, um objeto global `window.saastostore` dá acesso a recursos nativos. Em um navegador normal esse objeto não existe, e o site deve continuar funcionando exatamente como hoje. Não altere mais nada.
1. Crie `src/lib/saastostore.ts` com:
- `isInApp()`: retorna `typeof window !== "undefined" && navigator.userAgent.includes("SaasToStoreApp")`.
- `whenSaasToStore(key, timeoutMs = 3000)`: retorna `window.saastostore?.[key]` assim que existir (verificação a cada 100 ms), ou `null` após o prazo. A ponte só carrega quando a página termina de carregar.
- Os tipos TypeScript de `window.saastostore`, todos os membros opcionais:
- `biometric.isAvailable(): Promise<{ available: boolean; type: string } | null>`
- `biometric.authenticate(reason?: string): Promise<{ success: boolean; error: string | null } | null>`
- `setTabBadge(index: number, count: number): void`
- `billing: { isAvailable: boolean; identify(userId: string): void; purchase(productId: string): void; restore(): void; getStatus(): void }`
- `type PluginResult = { ok: boolean; error?: string } | null`
- `haptics: { impact(o?: { style?: "light" | "medium" | "heavy" }): Promise<PluginResult>; notification(o?: { type?: "success" | "warning" | "error" }): Promise<PluginResult>; selection(): Promise<PluginResult>; vibrate(o?: { duration?: number }): Promise<PluginResult> }`
- `appReview: { request(): Promise<{ requested: boolean; error?: string } | null> }`
- `share(o?: { title?: string; text?: string; url?: string }): Promise<{ shared: boolean; error?: string } | null>` (a function, not an object; with neither text nor url it shares the page shown; url must be http or https)
- `screen: { keepAwake(on?: boolean): Promise<PluginResult>; setBrightness(value?: number | null): Promise<PluginResult>; getBrightness(): Promise<{ brightness: number | null; error?: string } | null> }`
- `secureStorage: { get(key: string): Promise<{ value: string | null; error?: string } | null>; set(key: string, value: string): Promise<PluginResult>; remove(key: string): Promise<PluginResult>; keys(): Promise<{ keys: string[]; error?: string } | null>; clear(): Promise<PluginResult> }` (key: letters, digits, ".", "_", "-", 1 to 128 characters, e.g. "auth.refresh_token", never "auth:token"; value: a string of 64 KB at most, JSON.stringify objects; anything else resolves to { ok: false, error: "invalid_argument" }, nothing is converted)
- `appInfo: { get(): Promise<{ platform: "android" | "ios"; version: string; build: string; packageName: string; osVersion: string; language: string; model: string } | null> }`
- `browser: { open(url: string): Promise<PluginResult> }`
- `statusBar: { setStyle(style: "light" | "dark"): Promise<PluginResult>; setColor(hex: string): Promise<PluginResult> }` (setColor only works up to Android 14: call it with the page background together with setStyle, and ignore its not_supported)
- `localNotifications: { checkPermission(): Promise<{ status: "granted" | "denied" | "prompt" } | null>; requestPermission(): Promise<{ granted: boolean; status: "granted" | "denied" | "prompt"; error?: string } | null>; schedule(o: { id?: number; title: string; body?: string; at?: number | string; in?: number; data?: object }): Promise<{ scheduled: true; id: number; at: number } | { scheduled: false; error: string; status?: "denied" | "prompt" } | null>; cancel(o: { id: number }): Promise<PluginResult>; cancelAll(): Promise<PluginResult>; getPending(): Promise<{ notifications: { id: number; title: string; body: string; at: number; data: object }[] } | null>; getLastOpened(): Promise<{ notification: { id: number; title: string; body: string; at: number; data: object } | null } | null> }` (schedule() needs the permission: without it { scheduled: false, error: "not_allowed", status }; at: epoch ms or ISO 8601 with a time zone, or in: seconds, never both; title 1 to 200 characters, body 1,000 at most, data a JSON object of 4 KB at most; a tap dispatches a "s2s:localNotifications" CustomEvent on window with detail { type: "tap", id, title, body, at, data }; call getLastOpened() when the page starts: it returns the notification that opened the app, once)
- `barcode: { isAvailable(): Promise<{ available: boolean; reason?: "no_play_services" | "module_not_installed" | "no_camera" } | null>; checkPermission(): Promise<{ status: "granted" | "denied" | "prompt" } | null>; requestPermission(): Promise<{ granted: boolean; status: "granted" | "denied" | "prompt"; error?: string } | null>; scan(o?: { formats?: string[]; prompt?: string }): Promise<{ scanned: true; value: string; format: string } | { scanned: false; error: string; reason?: string } | null> }` (formats among qr, ean13, ean8, upca, upce, code128, code39, code93, itf, codabar, datamatrix, pdf417, aztec, all by default; prompt 200 characters at most, shown on iOS only; Android: Google Play services reader, no camera permission; iOS: the app's own reader after the camera permission)
- `downloads: { isAvailable(): Promise<{ available: boolean; error?: string } | null>; download(o: { url: string; filename?: string; open?: boolean }): Promise<{ downloaded: boolean; id?: string; filename?: string; mimeType?: string; size?: number; error?: string; reason?: string } | null>; open(o: { id: string }): Promise<{ opened: boolean; error?: string } | null> }` (normal download links need no code; the window event `s2s:downloads` carries `{ state, id, filename, source, error? }`)
- `calendar: { isAvailable(): Promise<{ available: boolean; error?: string } | null>; addEvent(o: { title: string; start: string | number | Date; end?: string | number | Date; allDay?: boolean; location?: string; notes?: string }): Promise<{ ok: true; saved: boolean | null } | { ok: false; error: string } | null> }` (opens the phone's calendar editor, filled in: the user saves the event, the site reads nothing; start and end: ISO 8601 with a time zone, epoch ms or a Date, end defaults to one hour after start; with allDay: true, "YYYY-MM-DD" days, end = the last day, included; title 1 to 500 characters, location 1,000 at most, notes 8,000; saved is null on Android, true or false on iOS)
- `documentScanner: { isAvailable(): Promise<{ available: boolean; error?: string; reason?: "no_play_services" | "unsupported_device" | "module_not_installed" } | null>; scan(o?: { pages?: number; format?: "jpeg" | "pdf" }): Promise<{ ok: true; format: "jpeg" | "pdf"; pageCount: number; truncated: boolean; files: File[] } | { ok: false; error: string; reason?: string } | null> }` (opens the phone's document scanner: edges found, page straightened; pages 1 to 20, 10 by default; jpeg: one File per page, pdf: a single File, ready for a FormData upload; 32 MB at most per scan, otherwise too_large; Android: Google Play services scanner, no camera permission; iOS: asks for the camera the first time, refused → not_allowed)
- `contacts: { isAvailable(): Promise<{ available: boolean; error?: string } | null>; pick(o?: { field?: "phone" | "email" | "name" }): Promise<{ ok: true; contact: { name: string | null; phones: string[]; emails: string[] } } | { ok: false; error: string } | null> }` (opens the phone's own contact picker: the user chooses one contact and, for "phone" or "email", one value, returned alone in phones or emails; field defaults to "phone"; the site never reads the address book, no permission)
Os resultados das compras chegam como um evento de `window` chamado `"s2s:billing"`; `event.detail.type` é `identify_result`, `purchase_result`, `restore_result` ou `status`.
2. Bloqueio biométrico: na página de configurações, mostre um botão «Desbloquear com digital / Face ID» só quando `isInApp()` e `biometric.isAvailable()` retornar `available: true`. Salve a escolha no localStorage. Quando estiver ligado, esconda o conteúdo logado ao abrir o app e ao voltar para o primeiro plano (`visibilitychange`), chame `biometric.authenticate("Desbloquear [NOME DO APP]")` e mostre o conteúdo de novo só quando `success` for true, com um botão «Tentar de novo» caso contrário.
3. Badge de aba: sempre que meu número de não lidos mudar, chame `window.saastostore?.setTabBadge?.([ÍNDICE DA ABA], count)` (0 esconde o badge).
4. Assinaturas no app Android: quando `isInApp()` e `billing.isAvailable` forem true, chame `billing.identify(user.id)` após o login e `billing.getStatus()` após o evento `identify_result`. Nesse caso, meu botão de upgrade chama `billing.purchase("[ID DO PRODUTO]")` em vez do pagamento web; escute `s2s:billing` e libere o plano pago quando um evento `purchase_result` ou `status` tiver `isEntitled: true`. No app iOS (`isInApp()` e `/iPhone|iPad/.test(navigator.userAgent)`), esconda esse botão. Em um navegador normal, mantenha o pagamento atual.
5. Plugins nativos: só para os plugins que adicionei no SaasToStore (apague as outras linhas). Chame cada um de forma opcional (`window.saastostore?.haptics?.…`); a promise dele nunca rejeita.
- Resposta tátil: chame `haptics.impact({ style: "light" })` ao tocar em um botão principal, e `haptics.notification({ type: "success" })` ou `({ type: "error" })` quando um formulário der certo ou falhar.
- Avaliação no app: chame `appReview.request()` uma vez, depois de [UM MOMENTO DE SUCESSO], nunca a partir de um botão.
- Compartilhamento nativo: dentro do app, faça meus botões de compartilhar chamarem `share({ title, text, url })` em vez do código de compartilhamento web.
- Controle da tela: em [PÁGINA COM QR CODE OU INGRESSO], chame `screen.keepAwake(true)` e `screen.setBrightness(1)` ao abrir, depois `screen.setBrightness(null)` e `screen.keepAwake(false)` ao fechar.
- Armazenamento seguro: dentro do app, salve também [VALOR A PROTEGER] com `secureStorage.set(key, value)` (chave: só letras, dígitos, `.`, `_`, `-`, por exemplo `auth.refresh_token`; valor: uma string, `JSON.stringify` para um objeto; nunca passe `undefined`), leia com `secureStorage.get(key)` e chame `secureStorage.clear()` no logout.
- Informações do app: mostre a `version` e o `build` retornados por `appInfo.get()` na página de configurações.
- Navegador integrado: dentro do app, abra links para outros domínios (não os links de login) com `browser.open(url)` em vez de uma nova aba.
- Barra de status: nas páginas escuras chame `statusBar.setColor(FUNDO DA PÁGINA)` e depois `statusBar.setStyle("dark")`, nas claras `statusBar.setColor(FUNDO DA PÁGINA)` e depois `statusBar.setStyle("light")` (setColor só funciona até o Android 14; ignore o `not_supported` dele).
- Orientação da tela: nada a programar, é uma configuração de build.
- Leitor de QR e código de barras: em [O BOTÃO QUE LÊ], chame `barcode.scan({ formats: ["qr"], prompt: "Leia o seu código" })` e use `value` quando `scanned` for true; ignore `error: "cancelled"`, mostre uma mensagem para os outros erros; esconda o botão quando `barcode.isAvailable()` responder `available: false`.
- Download de arquivos: meus links de download funcionam no app sem código. Só para [O BOTÃO QUE EXPORTA UM ARQUIVO], dentro do app, chame `downloads.download({ url, filename })` e mostre uma mensagem quando `downloaded` for false.
- Calendário: em [O BOTÃO QUE ADICIONA UM EVENTO], dentro do app, chame `calendar.addEvent({ title, start, end, location, notes })` com `start` e `end` como `date.toISOString()` (ou `{ title, start: "YYYY-MM-DD", end: "YYYY-MM-DD", allDay: true }` para dias inteiros, `end` incluído); mostre uma mensagem quando `ok` for false; esconda o botão quando `calendar.isAvailable()` responder `available: false`; fora do app, o botão continua fazendo o que faz hoje.
- Scanner de documentos: em [O BOTÃO QUE ESCANEIA UM DOCUMENTO], dentro do app, chame `documentScanner.scan({ format: "pdf" })` e envie `files` como um arquivo escolhido no meu formulário; não faça nada quando `error` for `"cancelled"`, mostre uma mensagem para os outros erros; esconda o botão quando `documentScanner.isAvailable()` responder `available: false`; fora do app, mantenha meu campo de arquivo.
- Contatos: em [O BOTÃO AO LADO DO CAMPO DE TELEFONE], dentro do app, chame `contacts.pick({ field: "phone" })` (ou `"email"`) e coloque `contact.phones[0]` (ou `contact.emails[0]`) no campo quando a lista não estiver vazia; não faça nada quando `error` for `"cancelled"`; fora do app, esconda esse botão e deixe o campo como está.
- Notificações locais: em [AÇÃO QUE MERECE UM LEMBRETE], chame primeiro `localNotifications.requestPermission()` (uma vez, a partir desse botão, nunca na abertura), depois `localNotifications.schedule({ id, title, body, at })` com `at` como `Date.now() + …` ou `date.toISOString()`; escute `window.addEventListener("s2s:localNotifications", e => …)` e chame `localNotifications.getLastOpened()` ao iniciar a página, para abrir [A PÁGINA DO LEMBRETE]; chame `localNotifications.cancel({ id })` quando o lembrete não for mais necessário.Antes de colar, apague as etapas de que não precisa e substitua os valores entre [COLCHETES]. Depois, gere um novo build no SaasToStore se você ativou um novo recurso nativo.
Detectar o app
Cada chamada desta página só faz sentido dentro do app. Verifique primeiro, e seu site continua funcionando igual em um navegador normal.
O user agent contém «SaasToStoreApp»
Apps Android gerados desde julho de 2026 e apps iOS gerados desde 19 de outubro de 2026 o adicionam a cada requisição e ao navigator.userAgent:
const inApp = navigator.userAgent.includes("SaasToStoreApp");Use includes(), nunca uma comparação exata
A marca vem depois do user agent do sistema, com um espaçamento que muda conforme a plataforma. Seu servidor também pode lê-la, no cabeçalho User-Agent.
Depois verifique a própria API
window.saastostore só existe dentro do app, e cada recurso só quando está ligado. Teste o membro de que precisa, por exemplo window.saastostore?.biometric, antes de chamá-lo.
O que funciona onde
Os badges nas abas, a navegação e as compras in-app vêm com o Shell V2, a casca nativa (cabeçalho, barra de abas, menu lateral) que você liga no Shell Designer do seu projeto; os plugins da biblioteca funcionam com ou sem ele (no iOS, a barra de status só nos apps em tela cheia). A biometria também, só que num app iOS sem Shell V2 ela precisa também de um destes plugins: Resposta tátil, Avaliação no app, Compartilhamento nativo, Controle da tela, Armazenamento seguro, Informações do app, Navegador integrado, Barra de status ou Orientação da tela. Os recursos nativos entram no momento do build: depois de ligar um, gere um novo build. As mudanças do seu site aparecem no app na hora; as nativas, não. Nada disso existe no Android Build (Web) (TWA), que abre o seu site no Chrome: escolha o Android Build (Native) ou o build de Android TV.
| Recurso | App Android (build nativo) | App iOS |
|---|---|---|
| Detectar o app (user agent) | Builds desde julho de 2026 | Builds desde 19/10/2026 |
| Desbloqueio biométrico (biometric) | Builds desde 28/10/2026, com ou sem Shell V2 | Com Shell V2; sem ele, se um destes plugins for adicionado: haptics, appReview, share, screen, secureStorage, appInfo, browser, statusBar ou orientation (novo build) |
| Plugins da biblioteca (tátil, compartilhar, tela…) | Depois de adicionados, a partir do seu próximo build, com ou sem Shell V2 | Depois de adicionados, a partir do seu próximo build, com ou sem Shell V2 (barra de status: só apps em tela cheia) |
| Badges nas abas (setTabBadge) | Shell V2 com barra de abas | Shell V2 com barra de abas |
| Aba ativa e título do cabeçalho | Shell V2 | Shell V2 |
| Compras in-app (billing) | Shell V2 + Assinaturas in-app (Google Play) | Ainda não disponível |
Esperar a ponte
A ponte carrega quando a página termina de carregar, então pode faltar nas primeiras linhas do seu código. Chame-a a partir de uma ação do usuário ou espere por ela com esta função auxiliar. Ela retorna o recurso pedido, ou null fora do app. Os plugins da biblioteca já estão lá desde a primeira linha da sua página.
whenSaasToStore(key)
function whenSaasToStore(key, timeoutMs = 3000) { return new Promise((resolve) => { const start = Date.now(); (function check() { const api = window.saastostore?.[key]; if (api || Date.now() - start > timeoutMs) return resolve(api ?? null); setTimeout(check, 100); })(); }); }
Desbloqueio biométrico
Pede a quem está com o celular que prove que é o dono: digital, rosto ou, na falta deles, o PIN do celular. Não faz login de ninguém: use para desbloquear uma sessão que seu site já tem ou para confirmar uma ação sensível. Ligue Desbloqueio biométrico na biblioteca de plugins do seu projeto (ligado por padrão) e gere um novo build. No iOS, ele também funciona sem Shell V2, desde que você adicione também um destes plugins: Resposta tátil, Avaliação no app, Compartilhamento nativo, Controle da tela, Armazenamento seguro, Informações do app, Navegador integrado, Barra de status ou Orientação da tela. O app o recebe no próximo build.
isAvailable()
Há uma digital ou um rosto cadastrado neste celular? type é fingerprint, face, iris ou biometric no Android, faceid ou touchid no iOS.
await window.saastostore.biometric.isAvailable(); // → { available: true, type: "fingerprint" } // → { available: false, type: "none" }authenticate(reason)
Abre a janela do sistema com o seu motivo abaixo do nome do app. error é a mensagem do sistema quando a pessoa cancela. No Android, também pode ser not_available (sem biometria nem bloqueio de tela) ou busy (já há uma janela aberta).
await window.saastostore.biometric.authenticate("Unlock your account"); // → { success: true, error: null } // → { success: false, error: "Authentication canceled" }Exemplo
const bio = await whenSaasToStore("biometric"); const check = await bio?.isAvailable(); if (check?.available) { const result = await bio.authenticate("Unlock your account"); if (result?.success) showAccount(); }
Plugins da biblioteca
Os plugins abaixo ficam desligados até você adicioná-los na biblioteca de plugins do seu projeto; depois, gere um novo build. Eles funcionam nos builds Android nativos (Android Build (Native), Android TV) e nos apps iOS, com ou sem Shell V2 (no iOS, a barra de status só nos apps em tela cheia). Eles não estão no Android Build (Web) (TWA): lá, window.saastostore não existe.
Prontos desde a primeira linha
A API deles é instalada antes do primeiro script da sua página, no domínio do app e nos subdomínios, só no frame principal: nunca em um iframe nem em outro site.
Promises que nunca rejeitam
Cada método retorna uma Promise que nunca rejeita. Quando a chamada falha, ela vale um objeto com um campo error: not_available, not_supported, invalid_argument, busy, timeout ou failed; e null se o app não puder responder. Fora do app, ou em um app gerado antes de o plugin ser adicionado, o plugin simplesmente não existe: verifique-o antes de chamá-lo.
const haptics = window.saastostore?.haptics; // undefined: normal browser, plugin not added, or app built before you added it if (haptics) { const r = await haptics.impact({ style: "light" }); if (r?.error) console.warn("haptics:", r.error); // never throws }
Resposta tátil
Vibrações curtas que confirmam um toque, um sucesso ou um erro. O que a pessoa sente depende do celular e da configuração de resposta tátil dele.
impact, notification, selection, vibrate
impact recebe um style (light, medium por padrão, heavy), notification um type (success por padrão, warning, error); selection é um leve tique, para um seletor. vibrate recebe uma duration de 1 a 1000 ms (300 por padrão); o iOS não permite definir a duração e toca um impacto forte no lugar. Uma duration que não seja um número de 1 a 1000 responde invalid_argument. No Android, impact, notification, selection e vibrate respondem not_available quando o aparelho não toca nada (sem vibrador, como a maioria dos tablets e TVs, ou com a resposta tátil desligada); o iOS responde ok: true nesse caso.
const haptics = window.saastostore?.haptics; await haptics?.impact({ style: "medium" }); // light | medium | heavy await haptics?.notification({ type: "success" }); // success | warning | error await haptics?.selection(); // a light tick await haptics?.vibrate({ duration: 200 }); // ms, 1 to 1000 // → { ok: true } // → { ok: false, error: "invalid_argument" } e.g. duration: 5000 // → { ok: false, error: "not_available" } Android: the device plays nothing
Avaliação no app
Abre a janela de avaliação da loja sem sair do app. A loja decide se ela aparece e limita a frequência; seu código nunca sabe se uma avaliação foi deixada. Ela só aparece em um app instalado pelo Google Play ou pela App Store, nunca no TestFlight.
request()
Chame depois de um sucesso (um pedido entregue, uma meta atingida), não como única ação de um botão «Avalie-nos»: a janela pode não aparecer. error pode ser not_available (sem Google Play no aparelho), busy (já há uma solicitação em andamento), timeout ou failed.requested: true volta assim que o pedido é passado à loja, sem esperar a janela fechar. No Android, um segundo pedido enquanto a primeira janela ainda está aberta responde busy.
// after a success, never as the only action of a button const r = await window.saastostore?.appReview?.request(); // → { requested: true } handed to the store, which decides whether to show it // → { requested: false, error: "not_available" } // → { requested: false, error: "busy" } Android: a review window is still open
Controle da tela
Mantenha a tela ligada e ajuste o brilho, para uma receita, um cartão de embarque ou um QR code no portão. As configurações valem enquanto o app está aberto.
keepAwake(on)
true mantém a tela ligada enquanto o app está em primeiro plano; false devolve o controle ao celular.
setBrightness(value), getBrightness()
value vai de 0 a 1; null volta ao brilho do sistema. No Android, só a janela do app muda. No iOS, muda a tela inteira: o app restaura o brilho original quando vai para segundo plano e reaplica o seu ao voltar. Os valores são verificados, nunca convertidos: setBrightness recebe null ou um número de 0 a 1, keepAwake um booleano (sem argumento: true); qualquer outra coisa responde invalid_argument. No Android, enquanto o app não definir um brilho, getBrightness retorna uma aproximação da configuração do sistema.
const screen = window.saastostore?.screen; await screen?.keepAwake(true); // → { ok: true } await screen?.setBrightness(1); // 0 to 1, e.g. to show a QR code await screen?.getBrightness(); // → { brightness: 1 } await screen?.setBrightness(null); // back to the system brightness await screen?.keepAwake(false); await screen?.setBrightness("1"); // → { ok: false, error: "invalid_argument" }
Armazenamento seguro
Um pequeno armazenamento chave-valor criptografado pelo celular, para um token ou um segredo de que seu site precisa de uma abertura para outra. No Android, os valores são criptografados com AES-256-GCM usando uma chave do Android Keystore que nunca sai do aparelho. No iOS, eles vão para o Keychain, legíveis só por este app, só neste aparelho, após o primeiro desbloqueio.
get, set, remove, keys, clear
As chaves usam letras, dígitos, ponto, sublinhado e hífen (de 1 a 128 caracteres); os valores são strings de até 64 KB: salve objetos com JSON.stringify. Nada é convertido por você: uma chave em outro formato (como auth:token), um valor que não seja string (undefined, um número, um objeto) ou longo demais responde invalid_argument, e nada é salvo.
const vault = window.saastostore?.secureStorage; await vault?.set("refresh_token", token); // → { ok: true } const saved = await vault?.get("refresh_token"); // → { value: "…" }, or { value: null } when the key is missing await vault?.keys(); // → { keys: ["refresh_token"] } await vault?.remove("refresh_token"); // → { ok: true } await vault?.clear(); // e.g. at sign-out await vault?.set("session", JSON.stringify({ id, exp })); // objects: JSON.stringify await vault?.set("auth:token", undefined); // → { ok: false, error: "invalid_argument" } ":" in the key, value not a stringLimites a conhecer
No iOS, o Keychain sobrevive à desinstalação do app: uma reinstalação encontra os valores antigos. Chame clear() no logout, e não trate um armazenamento vazio como primeira instalação. No Android, tudo é apagado com o app e nunca entra no backup: uma reinstalação, ou um celular novo, começa com o armazenamento vazio. Cada site tem seu próprio armazenamento, como o localStorage: www.example.com e example.com compartilham um, help.example.com tem outro, e keys() e clear() só veem as chaves do site que chama. O JavaScript da sua página pode ler os valores: eles ficam protegidos no celular, não contra um script injetado no seu site.
Informações do app
A versão e o número de build do app, a plataforma, a versão do sistema e o idioma do celular: para sua página de configurações ou uma mensagem ao suporte.
get()
build é uma string nas duas plataformas. model é um nome de modelo genérico (o modelo no Android, «iPhone» no iOS): nenhum identificador do aparelho é retornado.
await window.saastostore?.appInfo?.get(); // → { platform: "android", version: "1.4.0", build: "12", // packageName: "com.example.app", osVersion: "14", // language: "fr-FR", model: "Pixel 8" }
Navegador integrado
Abre uma página em uma aba de navegador por cima do app: Custom Tabs no Android (ou o navegador padrão), Safari View Controller no iOS. Para páginas fora do seu domínio: termos, ajuda, o site de um parceiro.
open(url)
Só http e https. A página roda fora do seu app: ela não tem window.saastostore, nem o user agent SaasToStoreApp, nem a sessão do seu app. Deixe o login com Google ou Apple para o app, que já cuida disso. error pode ser invalid_argument, busy (iOS: outra folha está aberta), not_available ou failed. No Android, um link para um site que tem o próprio app no celular (um site de vídeos ou uma rede social, ou o seu próprio domínio quando os deep links estão ativos) pode abrir esse app em vez de uma aba.
const r = await window.saastostore?.browser?.open("https://example.com/terms"); // → { ok: true } // → { ok: false, error: "invalid_argument" } not http or https
Barra de status
Ajuste a barra de status à sua página: ícones escuros em uma página clara, ícones claros em uma página escura. Até o Android 14, a barra de status é uma faixa acima da página, não a própria página: chame setColor com a cor de fundo da sua página junto com setStyle. A partir do Android 15 e no iOS, a página passa por baixo da barra de status: setStyle basta, e a resposta not_supported de setColor pode ser ignorada.
setStyle(style)
"light" para uma página clara (ícones escuros), "dark" para uma página escura (ícones claros). No iOS com Shell V2, a resposta é not_supported: a barra de status segue o cabeçalho nativo.
setColor(hex)
Cor de fundo no formato #RRGGBB, só no Android 14 e anteriores. O Android 15 e posteriores desenham os apps de ponta a ponta, e o iOS não tem cor de barra de status: lá a chamada responde not_supported.
const bar = window.saastostore?.statusBar; // a light page: dark icons, and (Android 14 and earlier) a bar of the page color await bar?.setColor("#ffffff"); // → { ok: true } up to Android 14 await bar?.setStyle("light"); // → { ok: true } // a dark page: light icons await bar?.setColor("#101827"); await bar?.setStyle("dark"); // setColor → { ok: false, error: "not_supported" } Android 15+ and iOS: ignore it // setStyle → { ok: false, error: "not_supported" } iOS with Shell V2
Orientação da tela
Uma configuração de build, sem JavaScript: adicione Orientação da tela na biblioteca de plugins, escolha Retrato ou Paisagem e gere um novo build. O app fica nessa orientação (no Android, ainda pode girar 180°).
- O Android 16 ignora esse bloqueio em telas grandes (600 dp ou mais: tablets, dobráveis abertos) nos apps que têm como alvo a API 36, como os builds do SaasToStore: mantenha um layout responsivo.
- O bloqueio em retrato não é aplicado ao build de Android TV, que fica em paisagem.
Notificações locais
Lembretes que a sua página agenda no telefone e que aparecem na hora escolhida, mesmo com o app fechado: um compromisso, uma sessão que começa, um carrinho abandonado. Nada sai do seu site e nenhum servidor participa: não é push. A notificação abre o seu app, e a sua página sabe qual foi tocada.
requestPermission() e checkPermission()
Peça uma vez, a partir de uma ação do usuário (um botão “Me lembrar”), nunca na abertura: Android 13 ou mais recente e iOS mostram uma janela do sistema (Androids mais antigos respondem granted). A resposta é { granted, status }, com status granted, denied ou prompt. Depois de uma recusa, o sistema não pergunta de novo: leve o usuário às configurações do app. checkPermission() só lê o estado.
schedule({ title, body, at | in, id, data })
at é uma data (milissegundos, ou ISO 8601 com fuso horário, o que toISOString() devolve) ou in um atraso em segundos, nunca os dois; sem nenhum deles, a notificação aparece agora. A resposta é { scheduled: true, id, at }. Limites: título 200 caracteres, texto 1.000, data um objeto JSON de 4 KB, 64 lembretes pendentes, 366 dias de antecedência; uma data passada aparece agora; o id de um lembrete pendente o substitui. Sem a permissão, { scheduled: false, error: "not_allowed", status }: peça antes. Caso contrário, invalid_argument, too_many ou failed.
const notif = window.saastostore?.localNotifications; // ask once, from a "Remind me" button, never at launch const p = await notif?.requestPermission(); // { granted: true, status: "granted" } const r = await notif?.schedule({ id: 42, // optional: the same id replaces a pending reminder title: "Your session starts in 10 minutes", body: "Tap to open the room", at: Date.now() + 50 * 60 * 1000, // or in: 3000 (seconds), or an ISO 8601 date data: { roomId: "a1" }, }); // r → { scheduled: true, id: 42, at: 1762000000000 } // without the permission: { scheduled: false, error: "not_allowed", status: "prompt" } window.addEventListener("s2s:localNotifications", (e) => { if (e.detail.type === "tap") openRoom(e.detail.data.roomId); }); const opened = await notif?.getLastOpened(); // { notification } once, then { notification: null } await notif?.cancel({ id: 42 });Toque, lista, cancelamento
Um toque abre o app na sua página, que recebe um evento s2s:localNotifications em window (detail.type "tap", com id, title, body, at, data). Se o app estava fechado, o evento dispara antes de a sua página escutar: chame getLastOpened() ao iniciar a página, ele responde a notificação uma vez e depois null. getPending() lista os lembretes por vir; cancel({ id }) e cancelAll() os removem, inclusive os já mostrados.
Hora e reinício
O Android mostra o lembrete com alguns minutos de margem (alarme inexato, sem a permissão “Alarmes e lembretes”): serve para um compromisso, não para uma contagem regressiva ao segundo. O iOS é pontual. Os lembretes sobrevivem a um reinício do telefone: o Android os reagenda na inicialização, o iOS os mantém. No Android, a notificação leva o ícone do seu app e um canal “Notifications” que o usuário pode silenciar nas configurações do sistema.
Leitor de QR e código de barras
Leia um QR code ou um código de barras com a câmera, a partir da sua página: um ingresso, um produto, um link para abrir. No Android, o leitor é o do Google (serviços do Google Play), em tela cheia com zoom automático, e o seu app não pede nenhuma permissão de câmera. No iOS, o app abre o próprio leitor, depois da permissão de câmera.
isAvailable()
{ available: true }, ou { available: false, reason }: no_play_services (Android sem serviços do Google Play: Amazon Fire, Huawei), module_not_installed (Android: o Google Play baixa o leitor uma vez; o download fica pedido, tente de novo mais tarde), no_camera (iOS). Verifique antes de mostrar um botão de leitura.
scan({ formats, prompt })
Abre o leitor e responde com o primeiro código lido: { scanned: true, value, format }. formats limita o que é lido (tudo por padrão): qr, ean13, ean8, upca, upce, code128, code39, code93, itf, codabar, datamatrix, pdf417, aztec. prompt é o texto mostrado acima do visor no iOS (no máximo 200 caracteres; o Android mostra a interface do Google). Fechado pelo usuário: { scanned: false, error: "cancelled" }; senão not_available, not_allowed, busy, invalid_argument ou failed.
const scanner = window.saastostore?.barcode; const ok = await scanner?.isAvailable(); // { available: true } or { available: false, reason: "no_play_services" | "module_not_installed" | "no_camera" } const r = await scanner?.scan({ formats: ["qr"], prompt: "Scan your ticket" }); if (r?.scanned) openTicket(r.value); // { scanned: true, value: "...", format: "qr" } else if (r?.error !== "cancelled") showMessage(r?.error); // not_available, not_allowed, busy, invalid_argument, failedPermissão
Android: nenhuma, checkPermission() responde sempre granted. iOS: scan() pede a câmera na primeira vez; checkPermission() e requestPermission() permitem pedir a partir de um botão seu, com status granted, denied ou prompt. Depois de uma recusa, o iOS não pergunta de novo: leve o usuário às configurações do app.
Download de arquivos
Os links de download do seu site funcionam no app, sem código da sua parte: um link com o atributo download, um arquivo enviado como anexo, uma exportação criada na página (blob:, data:). Um arquivo atrás de um login também funciona: o app envia seus cookies. No Android o arquivo vai para a pasta Downloads do telefone, com a notificação do sistema. O iOS não tem essa pasta: o app abre a pré-visualização do sistema, de onde o usuário compartilha o arquivo ou salva em Arquivos.
Links
Nada a programar para os links do seu domínio. Um PDF, ou um tipo de arquivo que o app não sabe exibir, também é tratado como download. Um link para outro domínio continua abrindo o navegador. No Android, um arquivo produzido por uma requisição POST não é suportado: sirva-o por um link GET ou crie-o na página. No Android 9 e anteriores, o arquivo fica na pasta do app.
download({ url, filename, open })
Inicia um download a partir do seu código e responde ao terminar: { downloaded: true, id, filename, mimeType, size }. url: http, https, blob: ou data:, relativa ou absoluta. filename substitui o nome, que o app limpa. open: true abre o arquivo no fim. Em caso de falha, { downloaded: false, error }: invalid_argument, not_available, busy (5 downloads ao mesmo tempo) ou failed com um reason como http_404 ou too_large (100 MB no máximo para blob: e data:).
// Links need no code. From your own code: const files = window.saastostore?.downloads; const r = await files?.download({ url: "/invoices/42.pdf", filename: "invoice-42.pdf" }); if (r?.downloaded) await files.open({ id: r.id }); // { downloaded: true, id, filename, mimeType, size } else if (r) showMessage(r.reason ?? r.error); // invalid_argument, not_available, busy, failed window.addEventListener("s2s:downloads", (e) => { console.log(e.detail); // { state: "started" | "completed" | "failed", id, filename, source: "link" | "api", error? } });open({ id })
Abre um arquivo baixado desde que o app foi iniciado, com o id dado por download() ou pelo evento: { opened: true }. Caso contrário, not_found, no_app (Android: nenhum app abre esse tipo), busy (iOS: uma folha já está aberta), invalid_argument ou failed.
Evento
s2s:downloads em window, para os links e para download(): e.detail = { state, id, filename, source, error }, com state started, completed ou failed, e source link ou api. Serve para mostrar que um link tocado está sendo baixado. isAvailable() responde { available: true }.
Calendário
Adicione a partir da sua página um evento ao calendário do celular: uma reserva, uma aula, um webinar. O app abre o editor do próprio calendário, preenchido com o título, as datas, o local e as notas que você passar; o usuário confere e salva. Seu site nunca lê nem escreve no calendário. No Android e no iOS 17 ou posterior o app não pede nenhuma permissão; no iOS 15 e 16, o iOS pede acesso ao calendário na primeira vez.
addEvent({ title, start, end, location, notes })
Abre o editor. title é obrigatório (no máximo 500 caracteres). start e end: data e hora ISO 8601 com o fuso horário, milissegundos epoch ou um Date; sem end, o evento dura uma hora. location (no máximo 1.000 caracteres) e notes (8.000) são opcionais. O Android responde { ok: true, saved: null } assim que o editor abre: ele não diz se o evento foi salvo. O iOS responde quando o editor fecha, com saved true ou false.
const cal = window.saastostore?.calendar; // from a button, never at launch const r = await cal?.addEvent({ title: "Product demo", start: "2026-11-20T09:00:00+01:00", // ISO 8601 with a time zone, epoch ms or a Date end: "2026-11-20T10:30:00+01:00", // optional: one hour after start location: "Lyon", notes: "Room 4, bring your badge", }); // Android → { ok: true, saved: null } (Android does not say whether it was saved) // iOS → { ok: true, saved: true | false } when the editor closes if (r && !r.ok) showMessage(r.error); // invalid_argument, not_available, busy, not_allowed // all day, 20 to 22 November (end included): await cal?.addEvent({ title: "Trade fair", start: "2026-11-20", end: "2026-11-22", allDay: true });Eventos de dia inteiro
allDay: true, com dias como start e end: "2026-11-20", ou uma data da qual se usa o dia local. end é o último dia, incluído; sem ele, o evento dura um dia. Sem allDay, uma data sem hora é recusada: informe uma hora e um fuso horário.
Erros
{ ok: false, error }: invalid_argument (sem título, uma data ilegível, um fim antes do início), not_available (nenhum app de calendário no celular), busy (iOS: um editor já está aberto) ou not_allowed (iOS 15 e 16: acesso ao calendário negado; envie o usuário para os ajustes do app). isAvailable() responde { available: false } em um celular Android sem app de calendário: esconda então o seu botão. Sem repetição nem lembrete nesta versão.
Scanner de documentos
Digitalize documentos em papel a partir da sua página: um documento de identidade, um recibo, um formulário assinado. O scanner do próprio celular abre, encontra as bordas da página e a endireita; o usuário pode refazer uma página antes de confirmar. Sua página recebe as páginas como imagens JPEG ou como um único PDF, em objetos File prontos para enviar. No Android, o scanner pertence aos serviços do Google Play e o seu app não pede nenhuma permissão de câmera; no iOS, é o scanner da Apple, e o iOS pede acesso à câmera na primeira vez.
scan({ pages, format })
Abre o scanner e responde { ok: true, format, pageCount, truncated, files }. format: jpeg (padrão), um arquivo por página (scan-1.jpg, scan-2.jpg…), ou pdf, um único scan.pdf. pages: de 1 a 20, 10 por padrão. No Android, o scanner para nesse número; no iOS, o usuário pode digitalizar mais: as primeiras páginas são mantidas e truncated vale true. No máximo 32 MB por digitalização; senão, too_large.
const scanner = window.saastostore?.documentScanner; const ok = await scanner?.isAvailable(); // { available: true } or { available: false, reason } // from a button: the phone's own scanner opens const r = await scanner?.scan({ pages: 5, format: "pdf" }); // pages 1 to 20 (10 by default), "jpeg" (default) or "pdf" if (r?.ok) { const body = new FormData(); r.files.forEach((f) => body.append("scan", f)); // File objects: scan.pdf, or scan-1.jpg, scan-2.jpg… await fetch("/api/documents", { method: "POST", body }); } else if (r && r.error !== "cancelled") showMessage(r.error); // busy, not_allowed, not_available, too_large, invalid_argument, failedisAvailable()
{ available: true }, ou { available: false, reason }: no_play_services (Android sem serviços do Google Play: Amazon Fire, Huawei), unsupported_device (Android com menos de 1,7 GB de memória, ou um simulador iOS), module_not_installed (Android: o Google Play baixa o scanner uma vez; o download já foi pedido, tente de novo mais tarde). Verifique antes de mostrar um botão de digitalização.
Erros
{ ok: false, error }: cancelled (fechado sem nenhuma página), busy (já há uma digitalização aberta), not_allowed (iOS: acesso à câmera recusado; mande o usuário para os ajustes do app), not_available, too_large, invalid_argument (pages fora de 1 a 20, um formato diferente de jpeg ou pdf) ou failed. No Android, o scanner só tira fotos: a importação da galeria está desativada.
Contatos
Deixe o usuário preencher um campo a partir dos contatos: um número para convidar um amigo, um e-mail para compartilhar um arquivo. O app abre o seletor de contatos do celular; o usuário escolhe um contato e depois um número ou um endereço. Seu site recebe essa escolha e nada mais: o app nunca lê a agenda e não pede nenhuma permissão, no Android como no iOS.
pick({ field })
Abre o seletor e responde { ok: true, contact: { name, phones, emails } }. field diz o que volta: phone (padrão), o número escolhido, em phones; email, o endereço escolhido, em emails; name, só o nome. Só podem ser escolhidos contatos que tenham um número (ou um endereço). name é null para um contato sem nome e é cortado em 500 caracteres; a lista fica vazia quando o valor escolhido está vazio ou passa de 320 caracteres: verifique antes de usar.
const contacts = window.saastostore?.contacts; // from a button: the phone's own contact picker opens const r = await contacts?.pick({ field: "phone" }); // "phone" (default), "email" or "name" if (r?.ok && r.contact.phones.length) phoneInput.value = r.contact.phones[0]; // { name: "Ada Lovelace", phones: ["+33 6 12 34 56 78"], emails: [] } else if (r && r.error !== "cancelled") showMessage(r.error); // busy, not_available, invalid_argument, failedErros
{ ok: false, error }: cancelled (fechado pelo usuário), busy (já há um seletor aberto), not_available (Android: nenhum app de contatos no celular), invalid_argument (um field diferente de phone, email ou name) ou failed. isAvailable() sempre responde { available: true } no iOS; no Android, { available: false } quando o celular não tem app de contatos: esconda então o seu botão.
Badges nas abas
Mostre um badge em uma aba nativa, para mensagens não lidas ou a quantidade no carrinho. Ligue Distintivo de notificação para essa aba no Shell Designer.
setTabBadge(index, count)
index começa em 0, na ordem das suas abas. Um count acima de 0 mostra o badge, 0 o esconde. Conforme o estilo da barra, o badge mostra o número ou um ponto.
window.saastostore?.setTabBadge?.(2, unreadCount); // 3rd tab, 0 hides it
Navegação e aba ativa
A casca nativa segue seu site sozinha: cada mudança de endereço (links, history.pushState, botão voltar, mudanças de hash) destaca a aba cuja Regex URL corresponde, e o cabeçalho mostra o document.title da sua página. Nada a programar para apps de página única em React, Vue ou Next.js.
navigateTo(path)
O que as abas nativas e o menu lateral chamam. Um caminho carrega essa página, um caminho que começa com # muda o hash e um endereço https completo o abre.
window.saastostore?.navigateTo?.("/cart"); // loads /cart window.saastostore?.navigateTo?.("#/cart"); // hash routing (Bubble, Glide)
Compras in-app (Android)
Conteúdos digitais vendidos em um app Android precisam passar pelo faturamento do Google Play. A ponte vende suas assinaturas por esse caminho, com o RevenueCat. Ligue Assinaturas in-app (Google Play) na biblioteca de plugins com sua chave Android do RevenueCat (goog_…) e seu entitlement, mantenha o Shell V2 ligado e gere um novo build. O app iOS ainda não tem: esconda lá o seu botão de upgrade.
Chamadas
billing.identify(userId); // after sign-in, before any purchase billing.getStatus(); // is the subscription active? billing.purchase(productId); // opens the Google Play purchase sheet billing.restore(); // restore purchasesResultados: o evento «s2s:billing»
Cada chamada responde com um evento em window; event.detail.type diz qual. Um error com valor cancelled significa que a pessoa fechou a janela de compra.
identify_result { success, userId, error? } purchase_result { success, productId, isEntitled?, error? } restore_result { success, isEntitled, error? } status { isEntitled, entitlementId, expiresAt? }Exemplo
const billing = await whenSaasToStore("billing"); window.addEventListener("s2s:billing", (e) => { const d = e.detail; if (d.type === "identify_result" && d.success) billing.getStatus(); if (d.type === "status") setPremium(d.isEntitled); if (d.type === "purchase_result" && d.success) setPremium(d.isEntitled); }); if (billing?.isAvailable) billing.identify(currentUser.id); // after sign-in // your "Upgrade" button, inside the Android app: billing.purchase("premium_monthly");
Firebase Analytics
Envie seus eventos ao Google Analytics for Firebase, no seu próprio projeto Firebase. Adicione o plugin, adicione seus arquivos do Firebase no detalhe dele e gere um novo build. Sem esses arquivos, o plugin fica fora do app: ele nunca roda em um projeto compartilhado com outros apps. Nada é enviado antes do consentimento do seu site (setConsent, abaixo). O app não tem identificador de publicidade.
logEvent({ name, params })
Envia um evento. name: no máximo 40 caracteres, letras, dígitos e _, começando por uma letra, sem os prefixos firebase_, google_ e ga_. params: no máximo 25 valores, cada um um texto, um número ou um booleano (enviado como 1 ou 0). Um nome ou valor recusado responde { ok: false, error: "invalid_argument" }; caso contrário, { ok: true }.
const analytics = window.saastostore?.firebaseAnalytics; // undefined: normal browser, plugin not added, or app built before you added it if (analytics) { await analytics.logEvent({ name: "purchase", params: { value: 39, currency: "EUR" } }); }setUserId({ id }), setUserProperty({ name, value })
id: seu próprio identificador de usuário, no máximo 256 caracteres, nunca um endereço de e-mail. name de uma propriedade: no máximo 24 caracteres, mesmas regras de um evento; value: no máximo 36 caracteres. null apaga o valor.
setConsent({ analytics, ads })
Nada é coletado até que seu site chame este método com analytics: true, nem mesmo os eventos que o Firebase registra sozinho na abertura. Chame-o quando o visitante aceitar, ou quando sua página carregar se você não precisar de consentimento. O Firebase guarda a resposta neste telefone de uma abertura para outra. O armazenamento de publicidade e a personalização continuam recusados até ads: true. analytics: false interrompe de novo toda a coleta. Pedir o consentimento onde a lei exige fica por sua conta.
// once the visitor has accepted (or at page load if you need no consent) await window.saastostore?.firebaseAnalytics?.setConsent({ analytics: true });
Google Tag Manager
Seu contêiner GTM roda no app como na web: suas tags disparam na página e nada muda. Se o seu app tiver o plugin Firebase Analytics, mais uma tag copia seus eventos para ele.
Tag HTML personalizado
Crie uma tag do tipo HTML personalizado com este código:
<script> (function () { var analytics = window.saastostore && window.saastostore.firebaseAnalytics; if (!analytics) return; // normal browser, or app built without the plugin analytics.logEvent({ name: {{Event}}, params: { page_path: {{Page Path}} } }); })(); </script>Acionador
Use um acionador Evento personalizado com os eventos a copiar, por exemplo purchase|sign_up com Usar correspondência de regex. Não use Todas as páginas: o GTM envia então nomes como gtm.js, que o Analytics recusa.
Nomes de eventos
Até 40 caracteres: letras, números e sublinhados, começando por uma letra. Qualquer outro nome é recusado: a chamada responde { ok: false, error: "invalid_argument" } e a página não é afetada.
Só no app
Para disparar uma tag só no app, ou excluí-la, adicione uma variável JavaScript personalizado e use-a nas condições do acionador:
function () { return navigator.userAgent.indexOf("SaasToStoreApp") !== -1; }Consentimento
O banner de consentimento do seu site não chega sozinho ao Analytics. Chame setConsent em uma tag acionada depois da resposta do visitante, antes do primeiro evento:
<script> (function () { var analytics = window.saastostore && window.saastostore.firebaseAnalytics; if (analytics) analytics.setConsent({ analytics: true, ads: false }); })(); </script>
Ponte JavaScript — perguntas frequentes
Preciso escrever código nativo ou adicionar um SDK ao meu site?+
Não. A ponte faz parte dos apps que o SaasToStore gera. Seu site a chama em JavaScript, e você liga os recursos, ou adiciona plugins, na biblioteca de plugins do seu projeto.
Isso muda meu site em um navegador normal?+
Não. window.saastostore só existe dentro do app. No navegador, as verificações não retornam nada e seu site se comporta exatamente como antes.
Por que window.saastostore é undefined quando minha página começa?+
A parte Shell V2 da ponte carrega quando a página termina de carregar: chame-a a partir de uma ação do usuário ou espere por ela com a função whenSaasToStore desta página. Os plugins da biblioteca já estão lá desde a primeira linha da sua página.
O desbloqueio biométrico serve para fazer login?+
Não. Ele só confirma que quem está com o celular é o dono. Seu site mantém o próprio login; a biometria desbloqueia uma sessão existente ou confirma uma ação sensível.
O que preciso para compras in-app?+
Uma conta de desenvolvedor do Google Play onde suas assinaturas estão criadas, um projeto RevenueCat ligado a ela e a chave Android do RevenueCat na biblioteca de plugins. Depois, gere um novo build.
Meu app foi gerado antes de um recurso existir. O que faço?+
Gere um novo build. Os recursos nativos entram no momento do build; as mudanças do seu site aparecem sem ele.
Os plugins precisam do Shell V2?+
Não. A resposta tátil, a avaliação no app, o compartilhamento nativo, o controle da tela, o armazenamento seguro, as informações do app, o navegador integrado e a orientação da tela funcionam com ou sem Shell V2, no Android e no iOS. A barra de status funciona com ou sem Shell V2 no Android, mas no iOS só nos apps em tela cheia: com Shell V2, ela segue o header nativo. Adicione-os na biblioteca de plugins e gere um novo build do app (Android Build (Native) ou iOS, não o Android Build (Web)).
Por que appReview.request() não mostra nada?+
O Google Play e a App Store decidem quando a janela de avaliação aparece e limitam a frequência. Ela só aparece em um app instalado pela loja, nunca no TestFlight, e seu código nunca sabe se uma avaliação foi deixada.
Notificações locais são notificações push?+
Não. Uma notificação local é criada pela sua página no próprio telefone, na hora que você escolher, sem servidor e sem mensagem enviada: um lembrete, um compromisso, um carrinho abandonado. Ela precisa da mesma permissão que o push no Android 13 ou mais recente e no iOS, pedida a partir de uma ação do usuário. Uma mensagem enviada a todos os seus usuários de uma vez é push, outro plugin.
O leitor precisa da permissão de câmera?+
No Android, não: o leitor pertence aos serviços do Google Play, o seu app não declara nenhuma permissão de câmera nem mostra janela de permissão; ele precisa dos serviços do Google Play e, uma vez, do download do módulo do leitor. No iOS, sim: o app abre o próprio leitor e o iOS pede a câmera na primeira vez, com o texto definido pelo SaasToStore ("This app uses the camera to scan QR codes and barcodes.").
Os links de download precisam de código?+
Não. Depois de adicionar o plugin e refazer o build do app, os links de download do seu site funcionam como estão, incluindo as exportações criadas na página. Sem o plugin, nada é salvo: no Android o toque não faz nada; no iOS um zip não faz nada, e um PDF ou arquivo de texto substitui a página, sem botão de voltar. No Android o arquivo vai para a pasta Downloads; no iOS o app abre a pré-visualização do sistema, de onde o usuário o compartilha ou salva em Arquivos.
O plugin de calendário lê o calendário do usuário?+
Não. Ele abre o editor do calendário do celular, preenchido com o seu evento, e o usuário salva; o seu site não recebe nada do calendário. No Android e no iOS 17 ou posterior não aparece nenhuma janela de permissão. No iOS 15 e 16, o iOS pede acesso ao calendário na primeira vez, com o texto definido pelo SaasToStore ("This app adds events to your calendar when you ask it to.").
O plugin de contatos lê a agenda?+
Não. O seletor do próprio celular abre, e o seu site recebe só o que o usuário escolhe: um contato, com um número ou um endereço. Não há nenhuma janela de permissão, nem no Android nem no iOS, e o seu site nunca recebe uma lista de contatos.
O scanner de documentos precisa da permissão de câmera?+
No Android, não: o scanner pertence aos serviços do Google Play, então o seu app não declara nenhuma permissão de câmera; o celular precisa dos serviços do Google Play, de 1,7 GB de memória e, uma vez, do download do módulo do scanner. No iOS, sim: o iOS pede a câmera na primeira vez, com o texto definido pelo SaasToStore ("This app uses the camera to scan documents.", ou um texto comum para documentos e códigos quando o leitor de QR também é adicionado).
Páginas relacionadas
Primeiro transforme seu site em app
Comece com um build Android de teste grátis e depois adicione os recursos nativos de que precisa.
Começar um teste grátis