AppNative.js

Simple native Android features for web apps packaged with the PWA to APK Builder: back gesture, vibration, notifications, native storage, sharing and more. No Capacitor knowledge needed. The same code also runs in a normal browser using web fallbacks.

Getting started

Include the script in your HTML, either from this website:

<script src="https://finnfreitag.com/tools/pwabuilder/appnative-1.0.js"></script>

or download it and put it into your project (recommended for ZIP uploads, so your app also works offline):

<script src="appnative-1.0.js"></script>

Then use the global AppNative object:

AppNative.showToast("Hello from Android!");
AppNative.vibrate(200);

AppNative.onBack(() => {
  if (menuOpen) { closeMenu(); return true; }
});

How it works

  • APKs from the builder contain a native bridge (Capacitor) and a fixed set of native plugins. AppNative calls them for you.
  • In a normal browser, every function falls back to a web API (or does nothing and logs a warning), so you never need separate code for web and app.
  • All functions return Promises and never throw. Listener functions return a function that removes the listener.
  • It works for ZIP/HTML uploads and for apps built from a website URL; in URL mode, your website must include the script.
  • You can still use any Capacitor plugin API directly through window.Capacitor.Plugins for the plugins included in the APK (App, Haptics, Toast, LocalNotifications, Preferences, Share, Filesystem, Clipboard, Network, Browser).
  • The file name is versioned (appnative-1.0.js), so updates will never break existing apps.

Contents

Environment

isNativeApp

AppNative.isNativeApp(): boolean

Returns true when the page runs inside an APK built with the PWA Builder, false in a normal browser.

In the apptrue
In the browserfalse
if (AppNative.isNativeApp()) {
  document.body.classList.add("in-app");
}

Feedback

showToast

AppNative.showToast(text: string, options?: { long?: boolean }): Promise<void>

Shows a short message at the bottom of the screen. Set long: true to show it longer (about 3.5 s instead of 2 s).

In the appAndroid toast
In the browserSmall toast overlay drawn by the script
AppNative.showToast("Saved!");
AppNative.showToast("Level complete – well done!", { long: true });

vibrate

AppNative.vibrate(pattern: number | number[]): Promise<void>

Vibrates for the given number of milliseconds. An array is a pattern of vibrate and pause times: [vibrate, pause, vibrate, …].

In the appDevice vibrator
In the browsernavigator.vibrate (Android Chrome; not supported on iOS/desktop)
AppNative.vibrate(200);
AppNative.vibrate([100, 50, 100]); // buzz – pause – buzz

haptic

AppNative.haptic(style: "light" | "medium" | "heavy"): Promise<void>

A short haptic tap, e.g. for button presses or game events. Feels more subtle than vibrate().

In the appHaptic impact feedback
In the browserVery short navigator.vibrate pulse
button.addEventListener("click", () => AppNative.haptic("light"));

Notifications

scheduleNotification

AppNative.scheduleNotification({ at: Date, title: string, text: string, repeat?: "minute" | "hour" | "day" | "week" }): Promise<number | null>

Schedules a notification for the given date and resolves to its id (use it with cancelNotification). On first use, the user is asked for notification permission; if they deny it, the promise resolves to null. With repeat, the notification fires again every minute / hour / day / week at the time taken from at, starting with the next matching time.

In the appAndroid alarm: fires even when the app is closed and after a reboot
In the browserBrowser notification via a timer: only fires while the page is open
const id = await AppNative.scheduleNotification({
  at: new Date(Date.now() + 60_000),
  title: "Timer",
  text: "Your 1 minute timer is done!",
});

// Every day at 08:00
const tomorrow8 = new Date();
tomorrow8.setDate(tomorrow8.getDate() + 1);
tomorrow8.setHours(8, 0, 0, 0);
AppNative.scheduleNotification({ at: tomorrow8, title: "Daily Maze", text: "A new maze is waiting!", repeat: "day" });

cancelNotification

AppNative.cancelNotification(id: number): Promise<void>

Cancels a scheduled (or repeating) notification by the id returned from scheduleNotification.

In the appCancels the Android alarm
In the browserClears the timer
AppNative.cancelNotification(id);

showNotification

AppNative.showNotification(title: string, text: string): Promise<void>

Shows a notification right away. Asks for permission on first use.

In the appAndroid notification
In the browserBrowser notification (uses the service worker if one is registered)
AppNative.showNotification("Download finished", "Your level pack is ready.");

Back gesture & app lifecycle

onBack

AppNative.onBack(handler: () => boolean | void): () => void

Reacts to the Android back gesture / back button. Return true from the handler if you handled the event (e.g. closed a menu). If no handler returns true, the default happens: go back in the page history, or close the app if there is no history. Handlers registered later run first, so nested dialogs work naturally. Returns a function that removes the handler.

In the appSystem back gesture / back button
In the browserBrowser back button (via a history entry while a handler is registered)
const off = AppNative.onBack(() => {
  if (menuOpen) {
    closeMenu();
    return true; // handled, don't leave the page
  }
});

// later: off();

exitApp

AppNative.exitApp(): Promise<void>

Closes the app.

In the appFinishes the Android activity
In the browserwindow.close() (only works for windows opened by script)
quitButton.onclick = () => AppNative.exitApp();

onPause

AppNative.onPause(fn: () => void): () => void

Called when the app goes to the background (home button, app switch, screen off). Good place to pause games or save state. Returns a function that removes the listener.

In the appApp pause event
In the browserPage becomes hidden (visibilitychange)
AppNative.onPause(() => game.pause());

onResume

AppNative.onResume(fn: () => void): () => void

Called when the app comes back to the foreground. Returns a function that removes the listener.

In the appApp resume event
In the browserPage becomes visible again
AppNative.onResume(() => refreshData());

onNetworkChange

AppNative.onNetworkChange(fn: (online: boolean) => void): () => void

Called whenever the device goes online or offline. Returns a function that removes the listener.

In the appAndroid connectivity events
In the browseronline / offline events
AppNative.onNetworkChange(online => {
  banner.hidden = online;
});

Storage

storage

AppNative.storage.set(key: string, value: any): Promise<void>
AppNative.storage.get(key: string): Promise<any | null>
AppNative.storage.remove(key: string): Promise<void>

Saves values permanently on the device. Values are stored as JSON, so numbers, strings, booleans, arrays and objects come back with the same type. get resolves to null if the key doesn't exist.

In the appAndroid SharedPreferences
In the browserlocalStorage (keys prefixed with "appnative.")
await AppNative.storage.set("highscore", 42);
await AppNative.storage.set("settings", { sound: true, level: 3 });

const score = await AppNative.storage.get("highscore"); // 42
await AppNative.storage.remove("settings");

Sharing, clipboard & links

share

AppNative.share({ title?: string, text?: string, url?: string, image?: Blob | File | HTMLCanvasElement | string }): Promise<boolean>

Opens the system share sheet. You can share text, a link, an image, or all of them together. image can be a Blob/File, a <canvas> element (great for game screenshots), a data URL or an image URL. Resolves to true if the share sheet was used, false if it was cancelled or isn't available.

In the appAndroid share sheet (text and image)
In the browserWeb Share API (images where the browser supports file sharing); otherwise copies text and link to the clipboard
AppNative.share({ title: "Daily Maze", text: "I solved today's maze in 42s!", url: "https://example.com" });

// Share a screenshot of a canvas
AppNative.share({ text: "My new highscore!", image: document.querySelector("canvas") });

// Share an image file from your project
AppNative.share({ image: "images/badge.png" });

copyToClipboard

AppNative.copyToClipboard(text: string): Promise<boolean>

Copies text to the clipboard. Resolves to true on success.

In the appAndroid clipboard
In the browsernavigator.clipboard (with a fallback for older browsers)
await AppNative.copyToClipboard(inviteCode);
AppNative.showToast("Copied!");

openExternal

AppNative.openExternal(url: string): Promise<void>

Opens a link outside of your app, so the user doesn't navigate away from it inside the app's WebView.

In the appIn-app browser tab (Chrome Custom Tab)
In the browserNew browser tab
AppNative.openExternal("https://finnfreitag.com");

Screen & status bar

keepScreenOn

AppNative.keepScreenOn(enabled: boolean): Promise<void>

Prevents the screen from turning off while enabled, e.g. for timers, recipes or videos.

In the appKeep-screen-on window flag
In the browserScreen Wake Lock API (where supported)
AppNative.keepScreenOn(true);
// ...
AppNative.keepScreenOn(false);

setStatusBar

AppNative.setStatusBar({ color?: string, lightIcons?: boolean }): Promise<void>

Sets the color behind the Android status bar (and navigation bar) and whether its icons are light (for dark colors) or dark. If lightIcons is left out, it is chosen automatically from the color. Note: if your page uses viewport-fit=cover, your content is drawn behind the status bar and you color that area yourself in CSS (use var(--safe-area-inset-top)).

In the appSystem bar color and icon style
In the browserSets <meta name="theme-color">
AppNative.setStatusBar({ color: "#000000", lightIcons: true });
AppNative.setStatusBar({ color: "#ffffff" }); // dark icons chosen automatically
Support me | Give Feedback
Impressum/Imprint | Privacy Policy | Copyright © Finn Freitag 2024 - 2026, All rights reserved. | Other copyright notices