ENTWICKLER-DOKU

JavaScript-Bridge: native Funktionen aus Ihrer Website

Ihre App zeigt Ihre Live-Website. In der App bekommt Ihre Website zusätzlich window.saastostore: Entsperren per Fingerabdruck oder Face ID, haptisches Feedback, das Teilen-Menü, sicheren Speicher und die anderen Plugins der Bibliothek, Badges auf den nativen Tabs, In-App-Käufe über Google Play und einen verlässlichen Weg zu erkennen, dass sie in der App läuft. Kein nativer Code, kein SDK in Ihrer Website: ein paar Zeilen JavaScript oder der Prompt unten für Lovable.

Aktualisiert am 4. November 2026

Prompt für Lovable, Bolt oder Base44

Füge meinem Projekt die Unterstützung für die SaasToStore-App-Bridge hinzu.

Kontext: Meine Website läuft auch in einer nativen Mobile-App, die mit SaasToStore gebaut wurde. In dieser App gibt ein globales Objekt `window.saastostore` Zugriff auf native Funktionen. In einem normalen Browser existiert dieses Objekt nicht, und die Website muss genau wie heute weiter funktionieren. Ändere sonst nichts.

1. Lege `src/lib/saastostore.ts` an mit:
   - `isInApp()`: gibt `typeof window !== "undefined" && navigator.userAgent.includes("SaasToStoreApp")` zurück.
   - `whenSaasToStore(key, timeoutMs = 3000)`: liefert `window.saastostore?.[key]`, sobald es existiert (Prüfung alle 100 ms), oder `null` nach Ablauf der Zeit. Die Bridge lädt erst, wenn die Seite fertig geladen ist.
   - TypeScript-Typen für `window.saastostore`, alle Mitglieder optional:
   - `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)
   Kaufergebnisse kommen als `window`-Event namens `"s2s:billing"`; `event.detail.type` ist `identify_result`, `purchase_result`, `restore_result` oder `status`.

2. Biometrische Sperre: Zeige auf der Einstellungsseite einen Schalter „Mit Fingerabdruck / Face ID entsperren“ nur, wenn `isInApp()` gilt und `biometric.isAvailable()` `available: true` liefert. Speichere die Wahl im localStorage. Ist sie aktiv, blende den angemeldeten Inhalt beim Öffnen der App und bei der Rückkehr in den Vordergrund (`visibilitychange`) aus, rufe `biometric.authenticate("[APP-NAME] entsperren")` auf und zeige den Inhalt erst wieder, wenn `success` true ist, sonst mit einem Button „Erneut versuchen“.

3. Tab-Badge: Rufe bei jeder Änderung meiner Anzahl ungelesener Elemente `window.saastostore?.setTabBadge?.([TAB-INDEX], count)` auf (0 blendet das Badge aus).

4. Abos in der Android-App: Wenn `isInApp()` und `billing.isAvailable` true sind, rufe nach der Anmeldung `billing.identify(user.id)` und nach dem Event `identify_result` `billing.getStatus()` auf. Dort ruft mein Upgrade-Button `billing.purchase("[PRODUKT-ID]")` statt des Web-Checkouts auf; höre auf `s2s:billing` und schalte Premium frei, wenn ein `purchase_result`- oder `status`-Event `isEntitled: true` enthält. In der iOS-App (`isInApp()` und `/iPhone|iPad/.test(navigator.userAgent)`) blende den Upgrade-Button aus. In einem normalen Browser bleibt der bisherige Zahlungsablauf.

5. Native Plugins: nur für die Plugins, die ich in SaasToStore hinzugefügt habe (lösche die anderen Zeilen). Rufe jedes optional auf (`window.saastostore?.haptics?.…`); sein Promise lehnt nie ab.
   - Haptisches Feedback: Rufe `haptics.impact({ style: "light" })` beim Tippen auf einen Haupt-Button auf und `haptics.notification({ type: "success" })` oder `({ type: "error" })`, wenn ein Formular gelingt oder scheitert.
   - Bewertung in der App: Rufe `appReview.request()` einmal nach [EINEM ERFOLGSMOMENT] auf, nie über einen Button.
   - Natives Teilen: Lass in der App meine Teilen-Buttons `share({ title, text, url })` statt des Web-Teilen-Codes aufrufen.
   - Bildschirmsteuerung: Rufe auf [SEITE MIT QR-CODE ODER TICKET] beim Öffnen `screen.keepAwake(true)` und `screen.setBrightness(1)` auf, beim Schließen `screen.setBrightness(null)` und `screen.keepAwake(false)`.
   - Sicherer Speicher: Speichere in der App zusätzlich [ZU SCHÜTZENDER WERT] mit `secureStorage.set(key, value)` (Schlüssel: nur Buchstaben, Ziffern, `.`, `_`, `-`, etwa `auth.refresh_token`; Wert: eine Zeichenkette, Objekte mit `JSON.stringify`; übergib nie `undefined`), lies ihn mit `secureStorage.get(key)` und rufe beim Abmelden `secureStorage.clear()` auf.
   - App-Infos: Zeige `version` und `build` aus `appInfo.get()` auf der Einstellungsseite.
   - In-App-Browser: Öffne in der App Links zu anderen Domains (keine Anmelde-Links) mit `browser.open(url)` statt in einem neuen Tab.
   - Statusleiste: Rufe auf dunklen Seiten `statusBar.setColor(SEITENHINTERGRUND)` und dann `statusBar.setStyle("dark")` auf, auf hellen Seiten `statusBar.setColor(SEITENHINTERGRUND)` und dann `statusBar.setStyle("light")` (setColor wirkt nur bis Android 14; ignoriere sein `not_supported`).
   - Bildschirmausrichtung: nichts zu programmieren, es ist eine Build-Einstellung.
   - QR- und Barcode-Scanner: Rufe bei [DEM BUTTON, DER SCANNT] `barcode.scan({ formats: ["qr"], prompt: "Code scannen" })` auf und verwende `value`, wenn `scanned` true ist; ignoriere `error: "cancelled"`, zeige bei den anderen Fehlern eine Meldung; blende den Button aus, wenn `barcode.isAvailable()` mit `available: false` antwortet.
   - Datei-Downloads: Meine Download-Links funktionieren in der App ohne Code. Nur für [DEN BUTTON, DER EINE DATEI EXPORTIERT]: Rufe in der App `downloads.download({ url, filename })` auf und zeige eine Meldung, wenn `downloaded` false ist.
   - Kalender: Rufe bei [DEM BUTTON, DER EINEN TERMIN HINZUFÜGT] in der App `calendar.addEvent({ title, start, end, location, notes })` auf, mit `start` und `end` als `date.toISOString()` (oder `{ title, start: "YYYY-MM-DD", end: "YYYY-MM-DD", allDay: true }` für ganze Tage, `end` inklusive); zeige eine Meldung, wenn `ok` false ist; blende den Button aus, wenn `calendar.isAvailable()` `available: false` antwortet; außerhalb der App macht der Button weiter, was er heute macht.
   - Dokumentenscanner: Rufe bei [DEM BUTTON, DER EIN DOKUMENT SCANNT] in der App `documentScanner.scan({ format: "pdf" })` auf und lade `files` hoch wie eine im Formular gewählte Datei; tue nichts, wenn `error` `"cancelled"` ist, zeige bei anderen Fehlern eine Meldung; blende den Button aus, wenn `documentScanner.isAvailable()` `available: false` antwortet; außerhalb der App bleibt mein Datei-Feld.
   - Kontakte: Rufe bei [DEM BUTTON NEBEN DEM TELEFONFELD] in der App `contacts.pick({ field: "phone" })` (oder `"email"`) auf und setze `contact.phones[0]` (oder `contact.emails[0]`) in das Feld, wenn die Liste nicht leer ist; tue nichts, wenn `error` `"cancelled"` ist; außerhalb der App blende diesen Button aus und lass das Feld, wie es ist.
   - Lokale Benachrichtigungen: Rufe bei [AKTION, DIE EINE ERINNERUNG VERDIENT] zuerst `localNotifications.requestPermission()` auf (einmal, über diesen Button, nie beim Start), dann `localNotifications.schedule({ id, title, body, at })` mit `at` als `Date.now() + …` oder `date.toISOString()`; höre auf `window.addEventListener("s2s:localNotifications", e => …)` und rufe beim Start der Seite `localNotifications.getLastOpened()` auf, um [DIE SEITE DER ERINNERUNG] zu öffnen; rufe `localNotifications.cancel({ id })` auf, wenn die Erinnerung nicht mehr gebraucht wird.

Löschen Sie vor dem Einfügen die Schritte, die Sie nicht brauchen, und ersetzen Sie die Werte in [KLAMMERN]. Bauen Sie die App danach auf SaasToStore neu, wenn Sie eine neue native Funktion aktiviert haben.

Die App erkennen

Jeder Aufruf auf dieser Seite ergibt nur in der App Sinn. Prüfen Sie zuerst, dann funktioniert Ihre Website im normalen Browser unverändert weiter.

  • Der User Agent enthält „SaasToStoreApp“

    Android-Apps, die seit Juli 2026 gebaut wurden, und iOS-Apps, die seit dem 19. Oktober 2026 gebaut wurden, hängen ihn an jede Anfrage und an navigator.userAgent an:const inApp = navigator.userAgent.includes("SaasToStoreApp");

  • includes() verwenden, nie einen exakten Vergleich

    Die Markierung steht hinter dem User Agent des Systems, mit je nach Plattform unterschiedlichen Leerzeichen. Ihr Server kann sie auch lesen, im Header User-Agent.

  • Dann die API selbst prüfen

    window.saastostore existiert nur in der App, und jede Funktion nur, wenn sie aktiviert ist. Prüfen Sie das benötigte Mitglied, zum Beispiel window.saastostore?.biometric, bevor Sie es aufrufen.

Was wo funktioniert

Tab-Badges, Navigation und In-App-Käufe kommen mit Shell V2, der nativen Hülle (Header, Tab-Leiste, Seitenmenü), die Sie im Shell Designer Ihres Projekts einschalten; die Plugins der Bibliothek funktionieren mit oder ohne (unter iOS die Statusleiste nur in Vollbild-Apps). Biometrie ebenfalls, nur braucht sie in einer iOS-App ohne Shell V2 zusätzlich eines dieser Plugins: Haptisches Feedback, Bewertung in der App, Natives Teilen, Bildschirmsteuerung, Sicherer Speicher, App-Infos, In-App-Browser, Statusleiste oder Bildschirmausrichtung. Native Funktionen werden beim Build hinzugefügt: Starten Sie nach dem Aktivieren einen neuen Build. Änderungen an Ihrer Website erscheinen sofort in der App, native Änderungen nicht. Nichts davon gibt es im Android Build (Web) (TWA), der Ihre Website in Chrome öffnet: Wählen Sie den Android Build (Native) oder den Build für Android TV.

FunktionAndroid-App (nativer Build)iOS-App
App erkennen (User Agent)Builds seit Juli 2026Builds seit 19.10.2026
Biometrische Entsperrung (biometric)Builds seit 28.10.2026, mit oder ohne Shell V2Mit Shell V2; ohne, wenn eines dieser Plugins hinzugefügt ist: haptics, appReview, share, screen, secureStorage, appInfo, browser, statusBar oder orientation (neuer Build)
Plugins der Bibliothek (Haptik, Teilen, Bildschirm…)Nach dem Hinzufügen ab Ihrem nächsten Build, mit oder ohne Shell V2Nach dem Hinzufügen ab Ihrem nächsten Build, mit oder ohne Shell V2 (Statusleiste: nur Vollbild-Apps)
Tab-Badges (setTabBadge)Shell V2 mit Tab-LeisteShell V2 mit Tab-Leiste
Aktiver Tab und Header-TitelShell V2Shell V2
In-App-Käufe (billing)Shell V2 + In-App-Abonnements (Google Play)Noch nicht verfügbar

Auf die Bridge warten

Die Bridge lädt, sobald Ihre Seite fertig geladen ist; in den allerersten Zeilen Ihres Codes kann sie also fehlen. Rufen Sie sie aus einer Nutzeraktion auf oder warten Sie mit dieser Hilfsfunktion. Sie liefert die Funktion, außerhalb der App null. Die Plugins der Bibliothek sind ab der ersten Zeile Ihrer Seite da.

  • 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); })(); }); }

Biometrische Entsperrung

Bittet die Person, die das Telefon hält, sich auszuweisen: Fingerabdruck, Gesicht oder ersatzweise die PIN des Telefons. Damit meldet sich niemand an: Nutzen Sie es, um eine bestehende Sitzung Ihrer Website zu entsperren oder eine heikle Aktion zu bestätigen. Aktivieren Sie Biometrische Entsperrung in der Plugin-Bibliothek Ihres Projekts (standardmäßig an) und bauen Sie die App neu. Unter iOS funktioniert sie auch ohne Shell V2, sofern Sie auch eines dieser Plugins hinzufügen: Haptisches Feedback, Bewertung in der App, Natives Teilen, Bildschirmsteuerung, Sicherer Speicher, App-Infos, In-App-Browser, Statusleiste oder Bildschirmausrichtung. Die App bekommt sie mit ihrem nächsten Build.

  • isAvailable()

    Ist auf diesem Telefon ein Fingerabdruck oder ein Gesicht eingerichtet? type ist fingerprint, face, iris oder biometric unter Android, faceid oder touchid unter iOS.await window.saastostore.biometric.isAvailable(); // → { available: true, type: "fingerprint" } // → { available: false, type: "none" }

  • authenticate(reason)

    Öffnet das Systemfenster mit Ihrem Grund unter dem App-Namen. error ist die Systemmeldung, wenn die Person abbricht. Unter Android kann er auch not_available (weder Biometrie noch Displaysperre) oder busy (ein Fenster ist schon offen) sein.await window.saastostore.biometric.authenticate("Unlock your account"); // → { success: true, error: null } // → { success: false, error: "Authentication canceled" }

  • Beispiel

    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 aus der Bibliothek

Die folgenden Plugins sind aus, bis Sie sie in der Plugin-Bibliothek Ihres Projekts hinzufügen; starten Sie danach einen neuen Build. Sie funktionieren in den nativen Android-Builds (Android Build (Native), Android TV) und in iOS-Apps, mit oder ohne Shell V2 (unter iOS die Statusleiste nur in Vollbild-Apps). Im Android Build (Web) (TWA) sind sie nicht enthalten: Dort gibt es kein window.saastostore.

  • Ab der ersten Zeile bereit

    Ihre API steht bereit, bevor das erste Skript Ihrer Seite läuft, auf der Domain der App und ihren Subdomains, nur im Hauptframe: nie in einem iframe oder auf einer anderen Website.

  • Promises, die nie ablehnen

    Jede Methode gibt ein Promise zurück, das nie ablehnt. Scheitert der Aufruf, liefert es ein Objekt mit einem Feld error: not_available, not_supported, invalid_argument, busy, timeout oder failed; und null, wenn die App nicht antworten kann. Außerhalb der App oder in einer App, die vor dem Hinzufügen des Plugins gebaut wurde, fehlt das Plugin einfach: Prüfen Sie es vor dem Aufruf.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 }

Haptisches Feedback

Kurze Vibrationen, die ein Tippen, einen Erfolg oder einen Fehler bestätigen. Was die Person spürt, hängt von ihrem Telefon und dessen Einstellung für haptisches Feedback ab.

  • impact, notification, selection, vibrate

    impact nimmt einen style (light, standardmäßig medium, heavy), notification einen type (standardmäßig success, warning, error); selection ist ein leichtes Ticken für eine Auswahl. vibrate nimmt eine duration von 1 bis 1000 ms (standardmäßig 300); iOS kann keine Dauer einstellen und spielt stattdessen einen starken Impuls. Eine duration, die keine Zahl von 1 bis 1000 ist, ergibt invalid_argument. Unter Android antworten impact, notification, selection und vibrate mit not_available, wenn das Gerät nichts abspielt (kein Vibrationsmotor, wie bei den meisten Tablets und TVs, oder haptisches Feedback ausgeschaltet); iOS antwortet in diesem Fall ok: true.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

Bewertung in der App

Öffnet das Bewertungsfenster des Stores, ohne die App zu verlassen. Der Store entscheidet, ob es erscheint, und begrenzt, wie oft; Ihr Code erfährt nie, ob eine Bewertung abgegeben wurde. Es erscheint nur in einer App, die aus Google Play oder dem App Store installiert wurde, nie in TestFlight.

  • request()

    Rufen Sie es nach einem Erfolg auf (eine gelieferte Bestellung, ein erreichtes Ziel), nicht als einzige Aktion eines Buttons „Bewerten Sie uns“: Das Fenster erscheint vielleicht nicht. error kann not_available (kein Google Play auf dem Gerät), busy (eine Anfrage läuft bereits), timeout oder failed sein.requested: true kommt zurück, sobald die Anfrage an den Store übergeben ist, ohne auf das Schließen des Fensters zu warten. Unter Android ergibt eine zweite Anfrage, solange das erste Fenster noch offen ist, 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

Natives Teilen

Öffnet das Teilen-Menü des Telefons mit einem Text, einem Link oder beidem. Nur Text und Links, keine Dateien. Nicht zu verwechseln mit Teilen empfangen, über das andere Apps Inhalte an Ihre senden.

  • share({ title, text, url })

    share ist eine Funktion, kein Objekt. Ohne text und url teilt es die Adresse der angezeigten Seite. url muss ein http- oder https-Link sein: Jeder andere Link (file:, mailto:, javascript:…) ergibt invalid_argument. title ist optional (der Betreff einer E-Mail, der Titel des Menüs, wenn das System einen zeigt). shared: true heißt, dass das Menü geöffnet wurde, nicht, dass etwas gesendet wurde.const r = await window.saastostore?.share?.({ title: "Trip to Lisbon", // optional text: "Here is our itinerary", url: "https://example.com/trips/42", }); // → { shared: true } the share sheet opened // → { shared: false, error: "invalid_argument" } url not http or https // share() with neither text nor url shares the page shown // in a normal browser: navigator.share?.({ title, text, url })

Bildschirmsteuerung

Halten Sie den Bildschirm an und stellen Sie seine Helligkeit ein, für ein Rezept, eine Bordkarte oder einen QR-Code am Gate. Die Einstellungen gelten, solange die App geöffnet ist.

  • keepAwake(on)

    true hält den Bildschirm an, solange die App im Vordergrund ist; false gibt die Kontrolle an das Telefon zurück.

  • setBrightness(value), getBrightness()

    value reicht von 0 bis 1; null kehrt zur Systemhelligkeit zurück. Unter Android ändert sich nur das Fenster der App. Unter iOS ändert sich der ganze Bildschirm: Die App stellt die ursprüngliche Helligkeit wieder her, wenn sie in den Hintergrund geht, und setzt Ihre bei der Rückkehr erneut. Werte werden geprüft, nie umgewandelt: setBrightness nimmt null oder eine Zahl von 0 bis 1, keepAwake einen Boolean (ohne Argument: true); alles andere ergibt invalid_argument. Unter Android gibt getBrightness, solange die App keine Helligkeit gesetzt hat, eine Näherung der Systemeinstellung zurück.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" }

Sicherer Speicher

Ein kleiner, vom Telefon verschlüsselter Schlüssel-Wert-Speicher für ein Token oder ein Geheimnis, das Ihre Website von einem Start zum nächsten braucht. Unter Android werden die Werte mit AES-256-GCM verschlüsselt, mit einem Schlüssel aus dem Android Keystore, der das Gerät nie verlässt. Unter iOS landen sie im Keychain, nur für diese App lesbar, nur auf diesem Gerät, nach dem ersten Entsperren.

  • get, set, remove, keys, clear

    Schlüssel bestehen aus Buchstaben, Ziffern, Punkt, Unterstrich und Bindestrich (1 bis 128 Zeichen); Werte sind Zeichenketten bis 64 KB: Speichern Sie Objekte mit JSON.stringify. Nichts wird für Sie umgewandelt: Ein Schlüssel in einem anderen Format (etwa auth:token), ein Wert, der keine Zeichenkette ist (undefined, eine Zahl, ein Objekt), oder ein zu langer ergibt invalid_argument, und nichts wird gespeichert.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 string

  • Grenzen, die Sie kennen sollten

    Unter iOS übersteht der Keychain die Deinstallation der App: Eine Neuinstallation findet die alten Werte wieder. Rufen Sie clear() beim Abmelden auf und werten Sie einen leeren Speicher nicht als Erstinstallation. Unter Android wird alles mit der App gelöscht und nie gesichert: Eine Neuinstallation oder ein neues Telefon beginnt mit leerem Speicher. Jede Website hat ihren eigenen Speicher, wie bei localStorage: www.example.com und example.com teilen sich einen, help.example.com hat einen eigenen, und keys() und clear() sehen nur die Schlüssel der aufrufenden Website. Das JavaScript Ihrer Seite kann die Werte lesen: Sie sind auf dem Telefon geschützt, nicht gegen ein in Ihre Website eingeschleustes Skript.

App-Infos

Version und Build-Nummer der App, Plattform, Systemversion und Sprache des Telefons: für Ihre Einstellungsseite oder eine Support-Nachricht.

  • get()

    build ist auf beiden Plattformen eine Zeichenkette. model ist ein allgemeiner Modellname (das Modell unter Android, „iPhone“ unter iOS): Es wird keine Gerätekennung zurückgegeben.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" }

In-App-Browser

Öffnet eine Seite in einem Browser-Tab über der App: Custom Tabs unter Android (oder der Standardbrowser), Safari View Controller unter iOS. Für Seiten außerhalb Ihrer Domain: AGB, Hilfe, die Website eines Partners.

  • open(url)

    Nur http und https. Die Seite läuft außerhalb Ihrer App: Sie hat weder window.saastostore noch den User-Agent SaasToStoreApp noch die Sitzung Ihrer App. Überlassen Sie die Anmeldung mit Google oder Apple der App, die sie bereits übernimmt. error kann invalid_argument, busy (iOS: ein anderes Blatt ist offen), not_available oder failed sein. Unter Android kann ein Link zu einer Website, deren eigene App auf dem Telefon installiert ist (eine Video- oder Social-Media-Website, oder Ihre eigene Domain bei aktiven Deep Links), diese App statt eines Tabs öffnen.const r = await window.saastostore?.browser?.open("https://example.com/terms"); // → { ok: true } // → { ok: false, error: "invalid_argument" } not http or https

Statusleiste

Passen Sie die Statusleiste an Ihre Seite an: dunkle Symbole auf einer hellen Seite, helle Symbole auf einer dunklen Seite. Bis Android 14 ist die Statusleiste ein Streifen über der Seite, nicht die Seite selbst: Rufen Sie zusammen mit setStyle auch setColor mit der Hintergrundfarbe Ihrer Seite auf. Ab Android 15 und unter iOS liegt die Seite unter der Statusleiste: setStyle genügt, und die Antwort not_supported von setColor können Sie ignorieren.

  • setStyle(style)

    "light" für eine helle Seite (dunkle Symbole), "dark" für eine dunkle Seite (helle Symbole). Unter iOS mit Shell V2 lautet die Antwort not_supported: Die Statusleiste folgt dem nativen Header.

  • setColor(hex)

    Hintergrundfarbe als #RRGGBB, nur unter Android 14 und früher. Ab Android 15 werden Apps randlos gezeichnet, und iOS hat keine Farbe für die Statusleiste: Dort antwortet der Aufruf 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

Bildschirmausrichtung

Eine Build-Einstellung, ohne JavaScript: Fügen Sie Bildschirmausrichtung in der Plugin-Bibliothek hinzu, wählen Sie Hochformat oder Querformat und starten Sie einen neuen Build. Die App bleibt in dieser Ausrichtung (unter Android kann sie sich noch um 180° drehen).

  • Android 16 ignoriert diese Sperre auf großen Bildschirmen (ab 600 dp: Tablets, aufgeklappte Foldables) bei Apps, die API 36 ansprechen, was SaasToStore-Builds tun: Halten Sie Ihr Layout responsiv.
  • Die Hochformat-Sperre wird im Android-TV-Build nicht angewendet; er bleibt im Querformat.

Lokale Benachrichtigungen

Erinnerungen, die Ihre Seite auf dem Telefon plant und die zur gewählten Zeit erscheinen, auch wenn die App geschlossen ist: ein Termin, eine Sitzung, die beginnt, ein stehen gelassener Warenkorb. Nichts verlässt Ihre Website, kein Server ist beteiligt: Das ist kein Push. Die Benachrichtigung öffnet Ihre App, und Ihre Seite erfährt, welche angetippt wurde.

  • requestPermission() und checkPermission()

    Fragen Sie einmal, aus einer Aktion des Nutzers heraus (ein Button „Erinnere mich“), nie beim Start: Android 13 und neuer sowie iOS zeigen ein Systemfenster (ältere Android-Versionen antworten granted). Die Antwort ist { granted, status }, mit status granted, denied oder prompt. Nach einer Ablehnung fragt das System nicht erneut: Schicken Sie den Nutzer in die App-Einstellungen. checkPermission() liest nur den Zustand.

  • schedule({ title, body, at | in, id, data })

    at ist ein Datum (Millisekunden oder ISO 8601 mit Zeitzone, wie toISOString() es liefert) oder in eine Verzögerung in Sekunden, nie beides; ohne eines von beiden erscheint die Benachrichtigung sofort. Die Antwort ist { scheduled: true, id, at }. Grenzen: Titel 200 Zeichen, Text 1.000, data ein JSON-Objekt von 4 KB, 64 ausstehende Erinnerungen, 366 Tage im Voraus; ein vergangenes Datum erscheint sofort; die id einer ausstehenden Erinnerung ersetzt sie. Ohne die Berechtigung { scheduled: false, error: "not_allowed", status }: Fragen Sie zuerst. Sonst invalid_argument, too_many oder 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 });

  • Antippen, Liste, Abbrechen

    Ein Tipp öffnet die App auf Ihrer Seite, die ein Ereignis s2s:localNotifications auf window erhält (detail.type "tap", mit id, title, body, at, data). War die App geschlossen, feuert das Ereignis, bevor Ihre Seite zuhört: Rufen Sie getLastOpened() beim Start der Seite auf, es antwortet einmal mit der Benachrichtigung, danach null. getPending() listet die kommenden Erinnerungen; cancel({ id }) und cancelAll() entfernen sie, angezeigte eingeschlossen.

  • Zeitpunkt und Neustart

    Android zeigt die Erinnerung auf einige Minuten genau (ungenauer Alarm, ohne die Berechtigung „Wecker und Erinnerungen“): richtig für einen Termin, nicht für einen Countdown auf die Sekunde. iOS ist pünktlich. Erinnerungen überleben einen Neustart des Telefons: Android plant sie beim Start neu, iOS behält sie. Auf Android trägt die Benachrichtigung Ihr App-Symbol und einen Kanal „Notifications“, den der Nutzer in den Systemeinstellungen stummschalten kann.

QR- und Barcode-Scanner

Lesen Sie einen QR-Code oder Barcode mit der Kamera, aus Ihrer Seite heraus: ein Ticket, ein Produkt, ein zu öffnender Link. Auf Android ist der Leser der von Google (Google-Play-Dienste), Vollbild mit Auto-Zoom, und Ihre App fragt nach keiner Kamera-Berechtigung. Auf iOS öffnet die App ihren eigenen Leser, nach der Kamera-Berechtigung.

  • isAvailable()

    { available: true } oder { available: false, reason }: no_play_services (Android ohne Google-Play-Dienste: Amazon Fire, Huawei), module_not_installed (Android: Google Play lädt den Leser einmal herunter; der Download ist angefordert, später erneut versuchen), no_camera (iOS). Prüfen Sie es, bevor Sie einen Scan-Button zeigen.

  • scan({ formats, prompt })

    Öffnet den Leser und antwortet mit dem ersten gelesenen Code: { scanned: true, value, format }. formats begrenzt, was gelesen wird (standardmäßig alles): qr, ean13, ean8, upca, upce, code128, code39, code93, itf, codabar, datamatrix, pdf417, aztec. prompt ist der Text über dem Sucher auf iOS (höchstens 200 Zeichen; Android zeigt Googles eigene Oberfläche). Vom Nutzer geschlossen: { scanned: false, error: "cancelled" }; sonst not_available, not_allowed, busy, invalid_argument oder 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, failed

  • Berechtigung

    Android: keine, checkPermission() antwortet immer granted. iOS: scan() fragt beim ersten Mal nach der Kamera; mit checkPermission() und requestPermission() fragen Sie über einen eigenen Button, mit status granted, denied oder prompt. Nach einer Ablehnung fragt iOS nicht erneut: Schicken Sie den Nutzer in die App-Einstellungen.

Datei-Downloads

Die Download-Links Ihrer Website funktionieren in der App, ohne Code auf Ihrer Seite: ein Link mit dem Attribut download, eine als Anhang gesendete Datei, ein in der Seite erzeugter Export (blob:, data:). Auch eine Datei hinter einer Anmeldung funktioniert: Die App sendet ihre Cookies. Auf Android landet die Datei im Downloads-Ordner des Telefons, mit der Systembenachrichtigung. iOS hat diesen Ordner nicht: Die App öffnet die Systemvorschau, aus der die Person die Datei teilt oder in Dateien sichert.

  • Links

    Für Links auf Ihrer Domain ist nichts zu programmieren. Ein PDF oder ein Dateityp, den die App nicht anzeigen kann, wird ebenfalls als Download behandelt. Ein Link auf eine andere Domain öffnet weiterhin den Browser. Auf Android wird eine Datei aus einer POST-Anfrage nicht unterstützt: Liefern Sie sie über einen GET-Link aus oder erzeugen Sie sie in der Seite. Auf Android 9 und älter bleibt die Datei im Ordner der App.

  • download({ url, filename, open })

    Startet einen Download aus Ihrem Code und antwortet, wenn er fertig ist: { downloaded: true, id, filename, mimeType, size }. url: http, https, blob: oder data:, relativ oder absolut. filename ersetzt den Namen, den die App bereinigt. open: true öffnet die Datei am Ende. Bei einem Fehler { downloaded: false, error }: invalid_argument, not_available, busy (5 Downloads gleichzeitig) oder failed mit einem reason wie http_404 oder too_large (höchstens 100 MB für blob: und 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 })

    Öffnet eine seit dem App-Start geladene Datei, mit der id aus download() oder aus dem Ereignis: { opened: true }. Sonst not_found, no_app (Android: keine App öffnet diesen Typ), busy (iOS: ein Fenster ist bereits offen), invalid_argument oder failed.

  • Ereignis

    s2s:downloads auf window, für Links und für download(): e.detail = { state, id, filename, source, error }, mit state started, completed oder failed und source link oder api. Damit zeigen Sie, dass ein berührter Link gerade lädt. isAvailable() antwortet { available: true }.

Kalender

Tragen Sie von Ihrer Seite aus einen Termin in den Kalender des Handys ein: eine Buchung, einen Kurs, ein Webinar. Die App öffnet den Editor des Kalenders selbst, vorausgefüllt mit Titel, Datum, Ort und Notizen, die Sie übergeben; der Nutzer prüft ihn und speichert. Ihre Website liest und schreibt nie im Kalender. Auf Android und ab iOS 17 fragt die App nach keiner Berechtigung; unter iOS 15 und 16 fragt iOS beim ersten Mal nach dem Kalenderzugriff.

  • addEvent({ title, start, end, location, notes })

    Öffnet den Editor. title ist Pflicht (höchstens 500 Zeichen). start und end: Datum und Uhrzeit nach ISO 8601 mit Zeitzone, Epoch-Millisekunden oder ein Date; ohne end dauert der Termin eine Stunde. location (höchstens 1.000 Zeichen) und notes (8.000) sind optional. Android antwortet { ok: true, saved: null }, sobald sich der Editor öffnet: Es meldet nicht, ob der Termin gespeichert wurde. iOS antwortet beim Schließen des Editors, mit saved true oder 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 });

  • Ganztägige Termine

    allDay: true, mit Tagen als start und end: "2026-11-20" oder ein Datum, dessen lokaler Tag zählt. end ist der letzte Tag, inklusive; ohne ihn dauert der Termin einen Tag. Ohne allDay wird ein Datum ohne Uhrzeit abgelehnt: Geben Sie Uhrzeit und Zeitzone an.

  • Fehler

    { ok: false, error }: invalid_argument (kein Titel, ein unlesbares Datum, ein Ende vor dem Beginn), not_available (keine Kalender-App auf dem Handy), busy (iOS: ein Editor ist schon offen) oder not_allowed (iOS 15 und 16: Kalenderzugriff verweigert; schicken Sie den Nutzer in die Einstellungen der App). isAvailable() antwortet { available: false } auf einem Android-Handy ohne Kalender-App: Blenden Sie Ihren Button dann aus. In dieser Version weder Wiederholung noch Erinnerung.

Dokumentenscanner

Scannen Sie Papierdokumente direkt aus Ihrer Seite: einen Ausweis, einen Kassenbon, ein unterschriebenes Formular. Der Scanner des Handys öffnet sich, erkennt die Ränder der Seite und richtet sie gerade aus; der Nutzer kann eine Seite vor dem Bestätigen neu aufnehmen. Ihre Seite erhält die Seiten als JPEG-Bilder oder als ein PDF, als File-Objekte, bereit zum Hochladen. Auf Android gehört der Scanner zu den Google-Play-Diensten, und Ihre App fragt nach keiner Kamera-Berechtigung; auf iOS ist es der Scanner von Apple, und iOS fragt beim ersten Mal nach der Kamera.

  • scan({ pages, format })

    Öffnet den Scanner und liefert { ok: true, format, pageCount, truncated, files }. format: jpeg (Standard), eine Datei pro Seite (scan-1.jpg, scan-2.jpg…), oder pdf, eine einzige scan.pdf. pages: 1 bis 20, standardmäßig 10. Auf Android hört der Scanner bei dieser Zahl auf; auf iOS kann der Nutzer mehr scannen: Die ersten Seiten werden behalten und truncated ist true. Höchstens 32 MB pro Scan, sonst 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, failed

  • isAvailable()

    { available: true } oder { available: false, reason }: no_play_services (Android ohne Google-Play-Dienste: Amazon Fire, Huawei), unsupported_device (Android mit weniger als 1,7 GB Arbeitsspeicher oder ein iOS-Simulator), module_not_installed (Android: Google Play lädt den Scanner einmalig herunter; der Download ist angefordert, versuchen Sie es später erneut). Prüfen Sie es, bevor Sie einen Scan-Button zeigen.

  • Fehler

    { ok: false, error }: cancelled (ohne Seite geschlossen), busy (ein Scan ist bereits geöffnet), not_allowed (iOS: Kamerazugriff verweigert; schicken Sie den Nutzer in die Einstellungen der App), not_available, too_large, invalid_argument (pages außerhalb von 1 bis 20, ein anderes Format als jpeg oder pdf) oder failed. Auf Android nimmt der Scanner nur Fotos auf: Der Import aus der Galerie ist deaktiviert.

Kontakte

Lassen Sie den Nutzer ein Feld aus seinen Kontakten füllen: eine Nummer, um einen Freund einzuladen, eine E-Mail-Adresse, um eine Datei zu teilen. Die App öffnet die Kontaktauswahl des Handys; der Nutzer wählt einen Kontakt, dann eine Nummer oder eine Adresse. Ihre Website erhält diese Auswahl und sonst nichts: Die App liest nie das Adressbuch und fragt nach keiner Berechtigung, auf Android wie auf iOS.

  • pick({ field })

    Öffnet die Auswahl und liefert { ok: true, contact: { name, phones, emails } }. field bestimmt, was zurückkommt: phone (Standard), die gewählte Nummer, in phones; email, die gewählte Adresse, in emails; name, nur der Name. Nur Kontakte mit einer Nummer (oder einer Adresse) sind wählbar. name ist null bei einem Kontakt ohne Namen und wird bei 500 Zeichen abgeschnitten; die Liste bleibt leer, wenn der gewählte Wert leer oder länger als 320 Zeichen ist: Prüfen Sie ihn vor der Verwendung.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, failed

  • Fehler

    { ok: false, error }: cancelled (vom Nutzer geschlossen), busy (eine Auswahl ist bereits geöffnet), not_available (Android: keine Kontakte-App auf dem Handy), invalid_argument (ein anderes field als phone, email oder name) oder failed. isAvailable() antwortet auf iOS immer { available: true }; auf Android { available: false }, wenn das Handy keine Kontakte-App hat: Blenden Sie dann Ihren Button aus.

Tab-Badges

Zeigen Sie ein Badge auf einem nativen Tab, für ungelesene Nachrichten oder die Anzahl im Warenkorb. Aktivieren Sie Benachrichtigungs-Badge für diesen Tab im Shell Designer.

  • setTabBadge(index, count)

    index beginnt bei 0, in der Reihenfolge Ihrer Tabs. Ein count über 0 zeigt das Badge, 0 blendet es aus. Je nach Stil der Tab-Leiste zeigt das Badge die Zahl oder einen Punkt.window.saastostore?.setTabBadge?.(2, unreadCount); // 3rd tab, 0 hides it

Navigation und aktiver Tab

Die native Hülle folgt Ihrer Website von selbst: Jeder Adresswechsel (Links, history.pushState, Zurück-Taste, Hash-Änderungen) hebt den Tab hervor, dessen URL-Regex passt, und der Header zeigt den document.title Ihrer Seite. Für Single-Page-Apps mit React, Vue oder Next.js ist nichts zu programmieren.

  • navigateTo(path)

    Das, was die nativen Tabs und das Seitenmenü aufrufen. Ein Pfad lädt diese Seite, ein Pfad mit # am Anfang ändert den Hash, eine vollständige https-Adresse wird geöffnet.window.saastostore?.navigateTo?.("/cart"); // loads /cart window.saastostore?.navigateTo?.("#/cart"); // hash routing (Bubble, Glide)

In-App-Käufe (Android)

Digitale Inhalte, die in einer Android-App verkauft werden, müssen über die Abrechnung von Google Play laufen. Die Bridge verkauft Ihre Abos darüber, mit RevenueCat. Aktivieren Sie In-App-Abonnements (Google Play) in den Native Features mit Ihrem Android-Schlüssel von RevenueCat (goog_…) und Ihrem Entitlement, lassen Sie Shell V2 an und bauen Sie neu. Die iOS-App hat es noch nicht: Blenden Sie dort Ihren Upgrade-Button aus.

  • Aufrufe

    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 purchases

  • Ergebnisse: das Event „s2s:billing“

    Jeder Aufruf antwortet mit einem Event auf window; event.detail.type sagt, welches. Ein error mit dem Wert cancelled heißt, dass die Person das Kauffenster geschlossen hat.identify_result { success, userId, error? } purchase_result { success, productId, isEntitled?, error? } restore_result { success, isEntitled, error? } status { isEntitled, entitlementId, expiresAt? }

  • Beispiel

    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

Senden Sie Ihre Events an Google Analytics for Firebase, in Ihrem eigenen Firebase-Projekt. Fügen Sie das Plugin hinzu, ergänzen Sie in seinen Details Ihre Firebase-Dateien und starten Sie einen neuen Build. Ohne diese Dateien bleibt das Plugin außerhalb der App: Es läuft nie in einem Projekt, das mit anderen Apps geteilt wird. Vor der Einwilligung Ihrer Seite wird nichts gesendet (setConsent, unten). Die App hat keine Werbe-ID.

  • logEvent({ name, params })

    Sendet ein Event. name: höchstens 40 Zeichen, Buchstaben, Ziffern und _, beginnt mit einem Buchstaben, ohne die Präfixe firebase_, google_ und ga_. params: höchstens 25 Werte, jeweils Text, Zahl oder Boolean (gesendet als 1 oder 0). Ein abgelehnter Name oder Wert antwortet { ok: false, error: "invalid_argument" }, sonst { 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: Ihre eigene Benutzer-ID, höchstens 256 Zeichen, niemals eine E-Mail-Adresse. name einer Eigenschaft: höchstens 24 Zeichen, gleiche Regeln wie bei einem Event; value: höchstens 36 Zeichen. null löscht den Wert.

  • setConsent({ analytics, ads })

    Nichts wird erfasst, bevor Ihre Seite diese Methode mit analytics: true aufruft, auch nicht die Events, die Firebase beim Start selbst protokolliert. Rufen Sie sie auf, wenn der Besucher zustimmt, oder beim Laden Ihrer Seite, wenn Sie keine Einwilligung brauchen. Firebase speichert die Antwort auf diesem Telefon von einem Start zum nächsten. Werbespeicher und Personalisierung bleiben bis ads: true abgelehnt. analytics: false beendet jede Erfassung wieder. Die Einwilligung dort einzuholen, wo das Gesetz sie verlangt, liegt bei Ihnen.// once the visitor has accepted (or at page load if you need no consent) await window.saastostore?.firebaseAnalytics?.setConsent({ analytics: true });

Google Tag Manager

Ihr GTM-Container läuft in der App wie im Web: Ihre Tags werden in der Seite ausgelöst, und nichts ändert sich. Hat Ihre App das Firebase Analytics-Plugin, kopiert ein weiterer Tag Ihre Ereignisse dorthin.

  • Tag „Benutzerdefiniertes HTML“

    Legen Sie einen Tag vom Typ Benutzerdefiniertes HTML mit diesem Code an:<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>

  • Trigger

    Verwenden Sie einen Trigger Benutzerdefiniertes Ereignis, der die zu kopierenden Ereignisse nennt, zum Beispiel purchase|sign_up mit Abgleich mit regulären Ausdrücken verwenden. Verwenden Sie nicht Alle Seiten: GTM sendet dann Namen wie gtm.js, die Analytics ablehnt.

  • Ereignisnamen

    Höchstens 40 Zeichen: Buchstaben, Ziffern und Unterstriche, am Anfang ein Buchstabe. Jeder andere Name wird abgelehnt: Der Aufruf antwortet { ok: false, error: "invalid_argument" }, und die Seite bleibt unberührt.

  • Nur in der App

    Um einen Tag nur in der App auszulösen oder dort auszuschließen, legen Sie eine Variable Benutzerdefiniertes JavaScript an und nutzen Sie sie in den Bedingungen des Triggers:function () { return navigator.userAgent.indexOf("SaasToStoreApp") !== -1; }

  • Einwilligung

    Das Einwilligungsbanner Ihrer Website erreicht Analytics nicht von selbst. Rufen Sie setConsent aus einem Tag auf, der ausgelöst wird, sobald der Besucher geantwortet hat, und zwar vor dem ersten Ereignis:<script> (function () { var analytics = window.saastostore && window.saastostore.firebaseAnalytics; if (analytics) analytics.setConsent({ analytics: true, ads: false }); })(); </script>

JavaScript-Bridge — häufige Fragen

Muss ich nativen Code schreiben oder ein SDK in meine Website einbauen?+

Nein. Die Bridge ist Teil der Apps, die SaasToStore baut. Ihre Website ruft sie in JavaScript auf, und Sie aktivieren die Funktionen oder fügen Plugins in der Plugin-Bibliothek Ihres Projekts hinzu.

Ändert das meine Website im normalen Browser?+

Nein. window.saastostore existiert nur in der App. Im Browser liefern die Prüfungen nichts, und Ihre Website verhält sich genau wie vorher.

Warum ist window.saastostore beim Start meiner Seite undefined?+

Der Shell-V2-Teil der Bridge lädt, sobald die Seite fertig geladen ist: Rufen Sie ihn aus einer Nutzeraktion auf oder warten Sie mit der Hilfsfunktion whenSaasToStore auf dieser Seite. Die Plugins der Bibliothek sind ab der ersten Zeile Ihrer Seite da.

Kann man sich mit der biometrischen Entsperrung anmelden?+

Nein. Sie bestätigt nur, dass die Person mit dem Telefon dessen Besitzer ist. Ihre Website behält ihre eigene Anmeldung; die Biometrie entsperrt eine bestehende Sitzung oder bestätigt eine heikle Aktion.

Was brauche ich für In-App-Käufe?+

Ein Google-Play-Entwicklerkonto, in dem Ihre Abos angelegt sind, ein damit verbundenes RevenueCat-Projekt und den Android-Schlüssel von RevenueCat in der Plugin-Bibliothek. Danach bauen Sie die App neu.

Meine App wurde gebaut, bevor es eine Funktion gab. Was tun?+

Starten Sie einen neuen Build. Native Funktionen kommen beim Build hinzu; Änderungen an Ihrer Website erscheinen ohne.

Brauchen die Plugins Shell V2?+

Nein. Haptisches Feedback, die Bewertung in der App, natives Teilen, Bildschirmsteuerung, sicherer Speicher, App-Infos, der In-App-Browser und die Bildschirmausrichtung funktionieren mit oder ohne Shell V2, unter Android und iOS. Die Statusleiste funktioniert unter Android mit oder ohne Shell V2, unter iOS aber nur in Vollbild-Apps: Mit Shell V2 folgt sie dem nativen Header. Fügen Sie sie in der Plugin-Bibliothek hinzu und bauen Sie die App neu (Android Build (Native) oder iOS, nicht der Android Build (Web)).

Warum zeigt appReview.request() nichts an?+

Google Play und der App Store entscheiden, wann das Bewertungsfenster erscheint, und begrenzen, wie oft. Es erscheint nur in einer aus dem Store installierten App, nie in TestFlight, und Ihr Code erfährt nie, ob eine Bewertung abgegeben wurde.

Sind lokale Benachrichtigungen Push-Benachrichtigungen?+

Nein. Eine lokale Benachrichtigung erstellt Ihre Seite auf dem Telefon selbst, zu einer Zeit Ihrer Wahl, ohne Server und ohne gesendete Nachricht: eine Erinnerung, ein Termin, ein stehen gelassener Warenkorb. Sie braucht auf Android 13 und neuer sowie auf iOS dieselbe Berechtigung wie Push, aus einer Aktion des Nutzers heraus erfragt. Eine Nachricht an alle Ihre Nutzer auf einmal ist Push, ein anderes Plugin.

Braucht der Scanner die Kamera-Berechtigung?+

Auf Android nein: Der Leser gehört zu den Google-Play-Diensten, Ihre App deklariert keine Kamera-Berechtigung und zeigt kein Berechtigungsfenster; sie braucht die Google-Play-Dienste und einmalig den Download des Leser-Moduls. Auf iOS ja: Die App öffnet ihren eigenen Leser und iOS fragt beim ersten Mal nach der Kamera, mit dem von SaasToStore gesetzten Text („This app uses the camera to scan QR codes and barcodes.“).

Brauchen Download-Links Code?+

Nein. Sobald das Plugin hinzugefügt und die App neu gebaut ist, funktionieren die Download-Links Ihrer Website unverändert, auch in der Seite erzeugte Exporte. Ohne das Plugin wird nichts gespeichert: Auf Android passiert beim Berühren nichts; auf iOS passiert bei einem ZIP nichts, und ein PDF oder eine Textdatei ersetzt die Seite, ohne Zurück-Schaltfläche. Auf Android landet die Datei im Downloads-Ordner; auf iOS öffnet die App die Systemvorschau, aus der die Person sie teilt oder in Dateien sichert.

Liest das Kalender-Plugin den Kalender des Nutzers?+

Nein. Es öffnet den Editor des Kalenders auf dem Handy, vorausgefüllt mit Ihrem Termin, und der Nutzer speichert ihn; Ihre Website bekommt nichts aus dem Kalender zurück. Auf Android und ab iOS 17 erscheint kein Berechtigungsfenster. Unter iOS 15 und 16 fragt iOS beim ersten Mal nach dem Kalenderzugriff, mit dem von SaasToStore gesetzten Text („This app adds events to your calendar when you ask it to.“).

Liest das Kontakte-Plugin das Adressbuch?+

Nein. Die Auswahl des Handys öffnet sich, und Ihre Website erhält nur, was der Nutzer wählt: einen Kontakt, mit einer Nummer oder einer Adresse. Es gibt kein Berechtigungsfenster, weder auf Android noch auf iOS, und Ihre Website erhält nie eine Liste von Kontakten.

Braucht der Dokumentenscanner die Kamera-Berechtigung?+

Auf Android nein: Der Scanner gehört zu den Google-Play-Diensten, Ihre App deklariert also keine Kamera-Berechtigung; das Handy braucht die Google-Play-Dienste, 1,7 GB Arbeitsspeicher und einmalig den Download des Scanner-Moduls. Auf iOS ja: iOS fragt beim ersten Mal nach der Kamera, mit dem von SaasToStore gesetzten Text („This app uses the camera to scan documents.“, oder einem gemeinsamen Text für Dokumente und Codes, wenn auch der QR-Scanner hinzugefügt ist).

Verwandte Seiten

Machen Sie Ihre Website zuerst zur App

Starten Sie mit einem kostenlosen Android-Test-Build und fügen Sie dann die nötigen nativen Funktionen hinzu.

Kostenlosen Test starten