Create Free APK

JavaScript bridge API

Sign-in and passkeys

A real Google account chooser, native rather than a web redirect, and passkeys through Android’s Credential Manager - navigator.credentials works as in Chrome, or use AppMint.passkey with your server’s JSON options.

AppMint.passkey helper#

window.AppMint.passkey — create(options), get(options), isAvailable(), origin(), assetLinks()

Example

Passkeys (sign in with fingerprint, face or screen lock - no password) through Android's passkey manager, using the JSON options your passkey server makes.

Returns: create(optionsJSON) → Promise of the RegistrationResponseJSON; get(optionsJSON) → Promise of the AuthenticationResponseJSON (send either to your server to verify). Both reject with a DOMException (err.name, and err.reason = a short code). isAvailable() → boolean, origin() → string, assetLinks() → array - all at once. Needs: Android 9+ with Google Play services, a passkey server, and the two set-up steps below. No build switch.

Sign in:

async function signInWithPasskey() {
  if (!window.AppMint || !window.AppMint.passkey || !window.AppMint.passkey.isAvailable()) {
    alert('Passkeys are not available on this phone.');
    return;
  }
  var options = await (await fetch('https://your-domain.com/passkey/login-options')).json();
  var answer;
  try {
    answer = await window.AppMint.passkey.get(options);   // rpId must be your domain
  } catch (err) {
    if (err.name === 'NotAllowedError') return;          // cancelled, or no passkey on this phone
    alert('Passkey problem: ' + err.message);
    return;
  }
  var res = await fetch('https://your-domain.com/passkey/login-verify', {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(answer)
  });
  if (res.ok) showAccount();
}

Create one (after the user signed in some other way):

async function addPasskey() {
  var options = await (await fetch('https://your-domain.com/passkey/register-options')).json();
  try {
    var answer = await window.AppMint.passkey.create(options);
    await fetch('https://your-domain.com/passkey/register-verify', {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(answer)
    });
  } catch (err) {
    if (err.name === 'InvalidStateError') alert('You already have a passkey here.');
    else if (err.name !== 'NotAllowedError') alert('Passkey problem: ' + err.message);
  }
}

Set up once - run this in the app installed FROM Google Play (Play signs with its own key):

console.log(JSON.stringify(window.AppMint.passkey.assetLinks(), null, 2));
// save as https://your-domain.com/.well-known/assetlinks.json
console.log(window.AppMint.passkey.origin());
// android:apk-key-hash:...  — add to your server's expected origins

Notes: err.name / err.reason: NotAllowedError (cancelled, no_credential, interrupted), InvalidStateError (already exists), SecurityError with not_linked (assetlinks.json does not list the app) or rp_mismatch (the rpId is not this page's domain), NotSupportedError / unavailable (Android < 9 or no Play services), TypeError / invalid_options (a page bundled in the app has no domain, so set rp.id / rpId to your domain). window.AppMint.passkey exists only in the app; in a browser use navigator.credentials (the same code works in the app too).

WebToApkAuth.signInWithGoogle helper#

window.WebToApkAuth.signInWithGoogle(webClientId)

Example

Signs the user in with their Google account through Android's own account picker, and gives you a Google ID token. Google blocks its sign-in page inside app WebViews ("disallowed_useragent"); this native path is not blocked.

Returns: a Promise of {ok:true, idToken, email, displayName, profilePictureUri}. It REJECTS with an Error whose message is the code (cancelled, google_play_services_unavailable, missing_web_client_id, failed, …). Needs: your OAuth Web client ID (ends in .apps.googleusercontent.com), and the app's SHA-1 and SHA-256 fingerprints registered with your Google / Firebase project (from the _signkey_info.txt file of your build, and Play's App signing key if you publish on Play). No build switch.

With Firebase Authentication (compat SDK):

var WEB_CLIENT_ID = '1234567890-abc.apps.googleusercontent.com';

document.getElementById('google-button').addEventListener('click', async function () {
  if (!window.WebToApkAuth) {
    alert('Google sign-in works in the app.');     // or run your normal web sign-in here
    return;
  }
  try {
    var r = await window.WebToApkAuth.signInWithGoogle(WEB_CLIENT_ID);
    var cred = firebase.auth.GoogleAuthProvider.credential(r.idToken);
    await firebase.auth().signInWithCredential(cred);
    showWelcome(r.displayName || r.email);
  } catch (err) {
    if (err.message === 'cancelled') return;                   // user closed the picker
    if (err.message === 'google_play_services_unavailable') {
      alert('No Google account or Google Play services on this phone.');
      return;
    }
    alert('Google sign-in failed: ' + err.message);
  }
});

With your own server - send the ID token and verify it there (never trust it in the page alone):

async function signInWithMyServer() {
  var r = await window.WebToApkAuth.signInWithGoogle(WEB_CLIENT_ID);
  await fetch('https://your-domain.com/auth/google', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ idToken: r.idToken })
  });
}

Notes: Pass the Web client ID. With no argument the helper uses window.firebaseConfig._googleWebClientId if your page sets it, then the Web client in the google-services.json you uploaded for this app (only when that file is your own Firebase project with Google sign-in enabled - never an AppMint Push build); with none of these it rejects with missing_web_client_id. A page that is not the app's own (an external site open inside the app) is refused with foreign_origin. Use the WEB client ID, not an Android client ID. "No credentials available" usually means the SHA-1 is not registered. This helper exists only in the app - in a browser use your normal web sign-in. Call it from a button tap.

WebToApkAuth.signOutGoogle helper#

window.WebToApkAuth.signOutGoogle()

Example

Clears Android's saved Google sign-in choice, so the next signInWithGoogle shows the account picker again instead of silently reusing the last account.

Returns: a Promise of {ok:true}; rejects with an Error whose message says what went wrong. Needs: nothing.

Call it together with your own sign-out:

document.getElementById('sign-out').addEventListener('click', async function () {
  await firebase.auth().signOut();            // your normal sign-out (Firebase here)
  if (window.WebToApkAuth) {
    try {
      await window.WebToApkAuth.signOutGoogle();
    } catch (err) {
      console.log('Could not clear the Google choice: ' + err.message);
    }
  }
  showSignInScreen();
});

Notes: This does not end your app's own session - sign the user out of your backend yourself. It only resets which Google account the phone offers next time.

navigator.credentials web standard#

navigator.credentials.create / get({ publicKey }) — passkeys

Example

Standard WebAuthn passkey calls. A normal Android WebView cannot do them; in the app they go to Android's passkey manager, so code and libraries written for Chrome (SimpleWebAuthn, Hanko, Corbado, Auth0) work unchanged.

Returns: a Promise of a PublicKeyCredential (with .toJSON()), exactly like Chrome; rejects with a DOMException. Needs: Android 9+ with Google Play services, a passkey server, your domain's assetlinks.json listing the app, and your server accepting the app's origin (see AppMint.passkey). No build switch.

Sign in with the standard API (options from your server, binary fields as ArrayBuffers):

async function signIn() {
  if (!window.PublicKeyCredential) { alert('Passkeys are not supported here.'); return; }
  var ok = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
  if (!ok) { alert('This phone cannot use passkeys.'); return; }

  try {
    var json = await (await fetch('https://your-domain.com/passkey/login-options')).json();
    var publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(json);  // provided in the app too
    var cred = await navigator.credentials.get({ publicKey: publicKey });
    await fetch('https://your-domain.com/passkey/login-verify', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(cred.toJSON())
    });
  } catch (err) {
    if (err.name !== 'NotAllowedError') alert('Passkey problem: ' + err.message);
  }
}

Register a passkey:

async function register() {
  if (!window.PublicKeyCredential) return;
  try {
    var json = await (await fetch('https://your-domain.com/passkey/register-options')).json();
    var cred = await navigator.credentials.create({
      publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(json)
    });
    await fetch('https://your-domain.com/passkey/register-verify', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(cred.toJSON())
    });
  } catch (err) {
    if (err.name === 'InvalidStateError') alert('You already have a passkey here.');
    else if (err.name !== 'NotAllowedError') alert('Passkey problem: ' + err.message);
  }
}

Notes: Passkey autofill in a text field (mediation: 'conditional') is not available in the app - isConditionalMediationAvailable() answers false and the call rejects with NotSupportedError; start sign-in from a button. signal (AbortController) works. A page bundled in the app has no domain of its own: set rp.id / rpId to your domain. Calls without publicKey (passwords, OTP) go to the WebView as before.

OTPCredential web standard#

navigator.credentials.get({ otp: { transport: ['sms'] } }) — WebOTP

Example

The standard WebOTP API: the login code from an SMS fills itself in. Android shows Google's "Allow this app to read this message?" sheet - no SMS permission.

Returns: a Promise of an OTPCredential {type:'otp', code}. Rejects AbortError when the user taps Deny or the signal aborts, InvalidStateError after five minutes without an SMS, NotSupportedError without Google Play services. Needs: nothing to switch on. The SMS must END with a line naming this page's host: @<host> #<code>.

const input = document.querySelector('input[autocomplete="one-time-code"]');

if ('OTPCredential' in window) {
  const ac = new AbortController();
  document.querySelector('form').addEventListener('submit', () => ac.abort());
  navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal })
    .then((otp) => { input.value = otp.code; input.form.requestSubmit(); })
    .catch((e) => console.log('Type the code instead:', e.name));
}

The SMS your server sends (last line is the binding):

Your Acme code is 482913.

@acme.example #482913

Notes: the host must be the page's own host - for a website-based app that is your domain (the SMS Chrome already reads); for an offline/AI-built app it is appassets.androidplatform.net. A message for another host is ignored and the app keeps waiting. Top page only (codes bound to an embedded origin, @top #code @iframe, are not supported). Only one request waits at a time; a newer one aborts the older. Event behind it: appmint:otp.

__otpCancel bridge#

window.WebToApk.__otpCancel(requestId: String)

Abandons a pending WebOTP request (AbortSignal, or a newer request replaced it).

Example

Internal transport behind aborting a WebOTP request (AbortSignal). Pages use the standard API; never call this directly - the __ methods may change without notice.

Returns: nothing. Needs: nothing to switch on.

Use the public API:

const ac = new AbortController();
const cred = await navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal });
codeInput.value = cred.code;
ac.abort();   // the pending get() rejects with AbortError

Notes: Implemented in the app shell (WebApiPolyfills.kt). The same page code works unchanged in Chrome.

__otpStart bridge#

window.WebToApk.__otpStart(requestId: String)

WebOTP: waits (up to five minutes) for one SMS through Google's SMS User Consent sheet - no SMS permission. The answer is `appmint:otp {requestId, message|error}`.

Example

Internal transport behind navigator.credentials.get({ otp: { transport: ['sms'] } }). Pages use the standard API; never call this directly - the __ methods may change without notice.

Returns: nothing; the answer arrives as the appmint:otp event {requestId, message} or {requestId, error} (cancelled, timeout, unavailable). Needs: nothing to switch on.

Use the public API:

const ac = new AbortController();
const cred = await navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal });
codeInput.value = cred.code;

Notes: Implemented in the app shell (WebApiPolyfills.kt). The same page code works unchanged in Chrome.

passkeyIsAvailable bridge#

window.WebToApk.passkeyIsAvailable(): Boolean

True on Android 9+ with Google Play services - the phones that can hold a passkey.

Example

Tells you whether this phone can use passkeys. Pages normally use the public API AppMint.passkey.isAvailable() (or PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()), which wraps this call.

Returns: a boolean, at once - true on Android 9+ with Google Play services and a WebView new enough for the passkey channel. Needs: nothing.

The public API:

var canPasskey = !!(window.AppMint && window.AppMint.passkey && window.AppMint.passkey.isAvailable());
document.getElementById('passkey-button').hidden = !canPasskey;

The raw call, if you need it:

var canPasskey = !!window.WebToApk &&
  typeof window.WebToApk.passkeyIsAvailable === 'function' &&
  window.WebToApk.passkeyIsAvailable() === true;

Notes: When this is false, AppMint.passkey.create/get reject with NotSupportedError. Hide the passkey button and offer your other sign-in.

passkeyOrigin bridge#

window.WebToApk.passkeyOrigin(): String

The origin a passkey server sees from this app: `android:apk-key-hash:<base64url SHA-256>`.

Example

Gives the origin your passkey server sees from this app: android:apk-key-hash:<base64url SHA-256 of the signing certificate>. Pages normally use the public API AppMint.passkey.origin(), which wraps this call.

Returns: a string, at once ("" if the certificate cannot be read). Needs: nothing.

Run this once in the app installed FROM Google Play, and add the value to your server's allowed origins:

if (window.AppMint && window.AppMint.passkey) {
  console.log(window.AppMint.passkey.origin());   // android:apk-key-hash:Lir5oIjf552K...
}

The raw call gives the same string:

if (window.WebToApk && typeof window.WebToApk.passkeyOrigin === 'function') {
  console.log(window.WebToApk.passkeyOrigin());
}

Notes: Google Play re-signs your app, so a test APK gives a different value than the Play install. SimpleWebAuthn example: expectedOrigin: ['https://your-domain.com', 'android:apk-key-hash:...'].

signInWithGoogle bridge#

window.WebToApk.signInWithGoogle(serverClientId: String, callbackId: String)

===== Native Google Sign-In via Credential Manager ===== Bypasses Google's Android WebView OAuth block (disallowed_useragent error) by running the sign-in flow in native Android and bridging just the resulting Google ID token back to JS, where the Firebase Web SDK consumes it via GoogleAuthProvider.credential(idToken) + signInWithCredential(...). JS contract: window.WebToApk.signInWithGoogle(serverClientId, callbackId) serverClientId "" = the Web client (type 3) in the creator's own google-services.json for this package; with none, error "missing_web_client_id". A page that is not the app's own gets error "foreign_origin". -> on completion, window.__webToApkAuth[callbackId](resultJsonString) is called resultJsonString = '{"ok":true,"idToken":"...","email":"...","displayName":"..."}' or '{"ok":false,"error":"<message>"}'

Example

Starts native Google sign-in (Android's account picker) and answers with a Google ID token. Pages normally use the Promise helper WebToApkAuth.signInWithGoogle(webClientId), which wraps this call.

Returns: nothing. The answer arrives later: the shell calls window.__webToApkAuthcallbackId with '{"ok":true,"idToken":"...","email":"...","displayName":"...","profilePictureUri":"..."}' or '{"ok":false,"error":"<code>","detail":"..."}', then removes that entry. Needs: your OAuth Web client ID, and the app's SHA-1 / SHA-256 registered with your Google or Firebase project. No build switch.

The helper (recommended):

async function googleSignIn() {
  if (!window.WebToApkAuth) return null;
  try {
    var r = await window.WebToApkAuth.signInWithGoogle('1234567890-abc.apps.googleusercontent.com');
    return r.idToken;
  } catch (err) {
    if (err.message !== 'cancelled') alert('Google sign-in failed: ' + err.message);
    return null;
  }
}

The raw call (argument order: web client ID, then your callback id):

function googleSignInRaw(done) {
  if (!window.WebToApk || typeof window.WebToApk.signInWithGoogle !== 'function') return;
  var id = 'g_' + Date.now();
  window.__webToApkAuth = window.__webToApkAuth || {};
  window.__webToApkAuth[id] = function (json) {
    var r = JSON.parse(json);
    if (r.ok) done(r.idToken, r.email);
    else if (r.error !== 'cancelled') alert('Google sign-in failed: ' + r.error);
  };
  window.WebToApk.signInWithGoogle('1234567890-abc.apps.googleusercontent.com', id);
}

Notes: Error codes: cancelled (user closed the picker), google_play_services_unavailable (no Google account or provider - also what a missing SHA-1 registration looks like), missing_web_client_id (none passed, and no Web client in the google-services.json you uploaded), foreign_origin (the page on screen is not the app's own), failed, unexpected_credential_type, parse_failed: .... An empty first argument uses the Web client from your own uploaded google-services.json when it has one. Verify the ID token on your server (or hand it to Firebase with GoogleAuthProvider.credential(idToken)).

signOutGoogle bridge#

window.WebToApk.signOutGoogle(callbackId: String)

Clears the saved Credential Manager state so the next signInWithGoogle call shows the account picker again (rather than silently re-selecting the last account). Generated apps should call this on user-initiated sign-out, in addition to firebase.auth().signOut().

Example

Clears Android's saved Google sign-in state, so the next sign-in shows the account picker again. Pages normally use the Promise helper WebToApkAuth.signOutGoogle(), which wraps this call.

Returns: nothing. The answer arrives later: the shell calls window.__webToApkAuthcallbackId with '{"ok":true}' or '{"ok":false,"error":"..."}'. Needs: nothing.

The helper (recommended):

async function signOut() {
  await myBackendSignOut();                          // end your own session first
  if (window.WebToApkAuth) {
    try { await window.WebToApkAuth.signOutGoogle(); } catch (err) { /* nothing to undo */ }
  }
}

The raw call:

if (window.WebToApk && typeof window.WebToApk.signOutGoogle === 'function') {
  var id = 'gout_' + Date.now();
  window.__webToApkAuth = window.__webToApkAuth || {};
  window.__webToApkAuth[id] = function (json) {
    var r = JSON.parse(json);
    if (!r.ok) console.log('Google sign-out failed: ' + r.error);
  };
  window.WebToApk.signOutGoogle(id);
}

Notes: It does not sign the user out of Firebase or your server - do that yourself.

Generated from the app runtime and its example files on every docs build. Read it as Markdown · All families.

Checked against the shipped bridge on 2026-09-23.