# Sign-in and passkeys - JavaScript bridge API

> 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.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/accounts

### `AppMint.passkey`

```js
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:

```js
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):

```js
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):

```js
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`

```js
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):

```js
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):

```js
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 })
  });
}
```

With Supabase Auth (hand the ID token to `signInWithIdToken`):

```js
async function signInWithSupabase() {
  if (!window.WebToApkAuth) {
    return supabase.auth.signInWithOAuth({ provider: 'google' });   // normal web sign-in in a browser
  }
  try {
    var r = await window.WebToApkAuth.signInWithGoogle(WEB_CLIENT_ID);
    var res = await supabase.auth.signInWithIdToken({ provider: 'google', token: r.idToken });
    if (res.error) throw res.error;
    showWelcome(res.data.user.email);
  } catch (err) {
    if (err.message !== 'cancelled') alert('Google sign-in failed: ' + err.message);
  }
}
```

**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 Appwright 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`

```js
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:

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

A "Switch account" button: clear the saved choice, then sign in again so the user picks another account:

```js
async function switchGoogleAccount() {
  if (!window.WebToApkAuth) return;
  await firebase.auth().signOut();
  try { await window.WebToApkAuth.signOutGoogle(); } catch (err) { /* nothing saved to clear */ }
  try {
    var r = await window.WebToApkAuth.signInWithGoogle('1234567890-abc.apps.googleusercontent.com');
    await firebase.auth().signInWithCredential(firebase.auth.GoogleAuthProvider.credential(r.idToken));
  } catch (err) {
    if (err.message !== 'cancelled') alert('Google sign-in failed: ' + err.message);
    showSignInScreen();
  }
}
```

Sign out of your own server first, then clear the Google choice without letting a failure block the logout:

```js
async function signOutEverywhere() {
  await fetch('https://your-domain.com/auth/logout', { method: 'POST', credentials: 'include' });
  localStorage.removeItem('profile');
  if (window.WebToApkAuth) {
    var r = await window.WebToApkAuth.signOutGoogle()
      .catch(function (err) { return { ok: false, error: err.message }; });
    if (!r.ok) console.log('Google choice not cleared: ' + r.error);
  }
  location.replace('/login');
}
```

**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`

```js
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):

```js
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:

```js
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);
  }
}
```

Decide which sign-in buttons to show from what the phone supports (passkey autofill is off in the app, so keep a button):

```js
async function setupSignInButtons() {
  if (!window.PublicKeyCredential) return;
  var caps = typeof PublicKeyCredential.getClientCapabilities === 'function'
    ? await PublicKeyCredential.getClientCapabilities() : {};
  var platform = caps.passkeyPlatformAuthenticator ||
    await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
  document.getElementById('passkey-button').hidden = !platform;
  if (caps.conditionalGet) startPasskeyAutofill();   // false in the app; true in some browsers
}
```

**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`

```js
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>`.

```js
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):

```text
Your Acme code is 482913.

@acme.example #482913
```

After "Resend code", start a fresh request (it replaces the one still waiting, which rejects with AbortError):

```js
async function resendAndWait(phone) {
  await fetch('/api/send-code', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone })
  });
  if (!('OTPCredential' in window)) return;
  try {
    const otp = await navigator.credentials.get({ otp: { transport: ['sms'] } });
    await verifyCode(otp.code);
  } catch (e) {
    if (e.name === 'InvalidStateError') showMessage('No SMS yet. Tap Resend to try again.');
  }
}
```

**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`

```js
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:**

```js
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
```

Stop waiting as soon as the user starts typing the code themselves:

```js
const input = document.querySelector('input[autocomplete="one-time-code"]');
const ac = new AbortController();
input.addEventListener('input', () => ac.abort(), { once: true });

navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal })
  .then((cred) => { input.value = cred.code; })
  .catch((e) => { if (e.name !== 'AbortError') console.log('No code:', e.name); });
```

Cancel the wait when the user goes back to change their phone number:

```js
let otpAbort = null;

function waitForSms() {
  otpAbort = new AbortController();
  return navigator.credentials.get({ otp: { transport: ['sms'] }, signal: otpAbort.signal });
}

document.getElementById('change-number').addEventListener('click', () => {
  if (otpAbort) { otpAbort.abort(); otpAbort = null; }   // the pending get() rejects with AbortError
  showPhoneStep();
});
```

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

### `__otpStart`

```js
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:**

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

Stop waiting after one minute with a timeout signal:

```js
async function waitForCode(input) {
  if (!('OTPCredential' in window)) return;
  try {
    const cred = await navigator.credentials.get({
      otp: { transport: ['sms'] },
      signal: AbortSignal.timeout(60000)
    });
    input.value = cred.code;
  } catch (e) {
    // TimeoutError after 60 s, AbortError when the user taps Deny
    showMessage('Type the code from the SMS.');
  }
}
```

Tell the user what to do next from the error name:

```js
async function readOtp() {
  try {
    const cred = await navigator.credentials.get({ otp: { transport: ['sms'] } });
    return cred.code;
  } catch (e) {
    if (e.name === 'InvalidStateError') showResendButton();      // no SMS within five minutes
    else if (e.name === 'NotSupportedError') showManualEntry();  // no Google Play services
    return null;                                                  // AbortError: the user tapped Deny
  }
}
```

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

### `passkeyAssetLinks`

```js
window.WebToApk.passkeyAssetLinks(): String
```

The assetlinks.json array (as text) that authorises this installed app for your domain.

**Example**

Builds the `assetlinks.json` content that tells Android your domain trusts this app (package name + the SHA-256 of the certificate the installed app is signed with). Pages normally use the public API `AppMint.passkey.assetLinks()`, which wraps this call and returns a parsed array.

**Returns:** a JSON array as TEXT, at once (`"[]"` if the certificate cannot be read). **Needs:** nothing.

Run once in the app installed FROM Google Play, then save the output as `https://your-domain.com/.well-known/assetlinks.json`:

```js
if (window.AppMint && window.AppMint.passkey) {
  console.log(JSON.stringify(window.AppMint.passkey.assetLinks(), null, 2));
}
```

The raw call returns the text already formatted:

```js
if (window.WebToApk && typeof window.WebToApk.passkeyAssetLinks === 'function') {
  var text = window.WebToApk.passkeyAssetLinks();
  console.log(text);
}
```

Read the package name and SHA-256 fingerprint out of the statement, for a console that asks for them one by one:

```js
if (window.AppMint && window.AppMint.passkey) {
  var statements = window.AppMint.passkey.assetLinks();
  if (statements.length === 0) {
    console.log('The signing certificate could not be read.');
  } else {
    var target = statements[0].target;
    console.log('Package: ' + target.package_name);
    console.log('SHA-256: ' + target.sha256_cert_fingerprints[0]);   // AB:CD:... upper-case, colon-separated
  }
}
```

**Notes:** The domain must be your server's rpID exactly (`www.` is a different domain), served over https with no redirect. If the file is wrong, passkey calls fail with `SecurityError` (reason `not_linked`).

### `passkeyIsAvailable`

```js
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:

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

The raw call, if you need it:

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

Offer "Add a passkey" once after a password sign-in, only on phones that can hold one (the standard check, same code in Chrome):

```js
async function offerPasskeyAfterLogin() {
  var ok = !!window.PublicKeyCredential &&
    await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
  if (!ok) return;                                   // Android below 9 or no Google Play services
  if (localStorage.getItem('passkey-offered')) return;
  localStorage.setItem('passkey-offered', '1');
  document.getElementById('add-passkey-banner').hidden = false;
}
```

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

### `passkeyOrigin`

```js
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:

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

The raw call gives the same string:

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

Show the origin on a hidden settings screen, so you can read it off the Play install without a debugger:

```js
var out = document.getElementById('passkey-origin');
var origin = (window.AppMint && window.AppMint.passkey) ? window.AppMint.passkey.origin() : '';

if (!origin) {
  out.textContent = 'Open this screen in the installed app.';
} else {
  out.textContent = origin;               // android:apk-key-hash:...
  out.style.userSelect = 'all';           // one long-press selects the whole value
}
```

**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`

```js
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.__webToApkAuth[callbackId](json)` 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):

```js
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):

```js
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);
}
```

The raw call with an empty client ID, which uses the Web client from the google-services.json you uploaded:

```js
function googleSignInFromUploadedFile() {
  return new Promise(function (resolve, reject) {
    if (!window.WebToApk || typeof window.WebToApk.signInWithGoogle !== 'function') {
      return reject(new Error('not_in_app'));
    }
    var id = 'g_' + Date.now();
    window.__webToApkAuth = window.__webToApkAuth || {};
    window.__webToApkAuth[id] = function (json) {
      var r = JSON.parse(json);
      if (r.ok) resolve(r); else reject(new Error(r.error));
    };
    window.WebToApk.signInWithGoogle('', id);
  });
}

googleSignInFromUploadedFile()
  .then(function (r) { showProfile(r.displayName, r.profilePictureUri); })
  .catch(function (e) {
    if (e.message === 'missing_web_client_id') console.log('Pass your Web client ID: the uploaded file has none.');
  });
```

**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`

```js
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.__webToApkAuth[callbackId](json)` with `'{"ok":true}'` or `'{"ok":false,"error":"..."}'`. **Needs:** nothing.

The helper (recommended):

```js
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:

```js
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);
}
```

The raw call wrapped in a Promise, used when the user deletes their account:

```js
function clearGoogleChoice() {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof window.WebToApk.signOutGoogle !== 'function') return resolve(false);
    var id = 'gout_' + Date.now();
    window.__webToApkAuth = window.__webToApkAuth || {};
    window.__webToApkAuth[id] = function (json) { resolve(JSON.parse(json).ok === true); };
    window.WebToApk.signOutGoogle(id);
  });
}

async function deleteAccount() {
  await fetch('https://your-domain.com/account', { method: 'DELETE', credentials: 'include' });
  await clearGoogleChoice();          // the next sign-in starts from the account picker
  showSignInScreen();
}
```

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

