Crear APK gratis

API del puente de JavaScript

Inicio de sesión y passkeys

Un auténtico selector de cuentas de Google, nativo en lugar de una redirección web, y passkeys mediante Credential Manager de Android: navigator.credentials funciona como en Chrome, o usa AppMint.passkey con las opciones JSON de tu servidor.

AppMint.passkey ayudante#

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

Ejemplo

Passkeys (inicia sesión con huella, rostro o bloqueo de pantalla, sin contraseña) mediante el administrador de passkeys de Android, usando las opciones JSON que genera tu servidor de passkeys.

Devuelve: create(optionsJSON) → Promise del RegistrationResponseJSON; get(optionsJSON) → Promise del AuthenticationResponseJSON (envía cualquiera a tu servidor para verificarlo). Ambos se rechazan con un DOMException (err.name y err.reason = un código corto). isAvailable() → booleano, origin() → cadena, assetLinks() → arreglo; todos de inmediato. Necesita: Android 9+ con Google Play Services, un servidor de passkeys y los dos pasos de configuración siguientes. Sin interruptor de compilación.

Iniciar sesión:

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

Crea una (después de que el usuario haya iniciado sesión de otra forma):

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

Configúralo una sola vez: ejecuta esto en la app instalada DESDE Google Play (Play firma con su propia clave):

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

Notas: err.name / err.reason: NotAllowedError (cancelled, no_credential, interrupted), InvalidStateError (ya existe), SecurityError con not_linked (assetlinks.json no incluye la app) o rp_mismatch (el rpId no es el dominio de esta página), NotSupportedError / unavailable (Android < 9 o sin Play Services), TypeError / invalid_options (una página incluida en la app no tiene dominio, así que define rp.id / rpId con tu dominio). window.AppMint.passkey solo existe en la app; en un navegador usa navigator.credentials (el mismo código también funciona en la app).

WebToApkAuth.signInWithGoogle ayudante#

window.WebToApkAuth.signInWithGoogle(webClientId)

Ejemplo

Inicia la sesión del usuario con su cuenta de Google mediante el selector de cuentas propio de Android y te da un token de ID de Google. Google bloquea su página de inicio de sesión dentro de los WebView de las apps («disallowed_useragent»); esta vía nativa no está bloqueada.

Devuelve: una Promise de {ok:true, idToken, email, displayName, profilePictureUri}. SE RECHAZA con un Error cuyo message es el código (cancelled, google_play_services_unavailable, missing_web_client_id, failed, …). Necesita: tu ID de cliente web de OAuth (termina en .apps.googleusercontent.com) y las huellas SHA-1 y SHA-256 de la app registradas en tu proyecto de Google / Firebase (del archivo _signkey_info.txt de tu compilación y de la clave de firma de la app de Play si publicas en Play). Sin interruptor de compilación.

Con Firebase Authentication (SDK compat):

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

Con tu propio servidor: envía el token de ID y verifícalo allí (nunca confíes en él solo en la página):

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

Con Supabase Auth (pasa el token de ID a signInWithIdToken):

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

Notas: Pasa el ID de cliente web. Sin argumento, el ayudante usa window.firebaseConfig._googleWebClientId si tu página lo define y luego el cliente web del google-services.json que subiste para esta app (solo cuando ese archivo es de tu propio proyecto de Firebase con el inicio de sesión con Google activado; nunca de una compilación de AppMint Push); si no hay ninguno de estos, se rechaza con missing_web_client_id. Una página que no es la propia de la app (un sitio externo abierto dentro de la app) se rechaza con foreign_origin. Usa el ID de cliente WEB, no un ID de cliente de Android. «No credentials available» suele significar que el SHA-1 no está registrado. Este ayudante solo existe en la app; en un navegador usa tu inicio de sesión web normal. Llámalo desde un toque en un botón.

WebToApkAuth.signOutGoogle ayudante#

window.WebToApkAuth.signOutGoogle()

Ejemplo

Borra la selección guardada de inicio de sesión con Google de Android, de modo que el siguiente signInWithGoogle vuelva a mostrar el selector de cuentas en lugar de reutilizar en silencio la última cuenta.

Devuelve: una Promise de {ok:true}; se rechaza con un Error cuyo message indica qué salió mal. Necesita: nada.

Llámalo junto con tu propio cierre de sesión:

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

Un botón «Cambiar de cuenta»: borra la selección guardada y luego vuelve a iniciar sesión para que el usuario elija otra cuenta:

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

Cierra primero la sesión en tu propio servidor y luego borra la selección de Google sin dejar que un fallo bloquee el cierre de sesión:

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');
}

Notas: Esto no cierra la sesión propia de tu app: cierra tú mismo la sesión del usuario en tu backend. Solo restablece qué cuenta de Google ofrece el teléfono la próxima vez.

navigator.credentials estándar web#

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

Ejemplo

Llamadas estándar de passkeys WebAuthn. Un WebView normal de Android no puede hacerlas; en la app van al administrador de passkeys de Android, así que el código y las bibliotecas escritos para Chrome (SimpleWebAuthn, Hanko, Corbado, Auth0) funcionan sin cambios.

Devuelve: una Promise de un PublicKeyCredential (con .toJSON()), igual que Chrome; se rechaza con un DOMException. Necesita: Android 9+ con Google Play Services, un servidor de passkeys, el assetlinks.json de tu dominio que incluya la app y tu servidor aceptando el origen de la app (consulta AppMint.passkey). Sin interruptor de compilación.

Inicia sesión con la API estándar (opciones de tu servidor, campos binarios como 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);
  }
}

Registrar una 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);
  }
}

Decide qué botones de inicio de sesión mostrar según lo que admita el teléfono (el autocompletado de passkeys está desactivado en la app, así que conserva un botón):

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
}

Notas: El autocompletado de passkeys en un campo de texto (mediation: 'conditional') no está disponible en la app: isConditionalMediationAvailable() responde false y la llamada se rechaza con NotSupportedError; inicia el inicio de sesión desde un botón. signal (AbortController) funciona. Una página incluida en la app no tiene dominio propio: define rp.id / rpId con tu dominio. Las llamadas sin publicKey (contraseñas, OTP) van al WebView como antes.

OTPCredential estándar web#

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

Ejemplo

La API estándar WebOTP: el código de inicio de sesión de un SMS se rellena solo. Android muestra la hoja de Google «¿Permitir que esta app lea este mensaje?»; sin permiso de SMS.

Devuelve: una Promise de un OTPCredential {type:'otp', code}. Se rechaza con AbortError cuando el usuario toca Denegar o la señal se cancela, con InvalidStateError tras cinco minutos sin SMS, con NotSupportedError sin Google Play Services. Necesita: nada que activar. El SMS debe TERMINAR con una línea que nombre el host de esta página: @<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));
}

El SMS que envía tu servidor (la última línea es la vinculación):

Your Acme code is 482913.

@acme.example #482913

Después de «Reenviar código», inicia una solicitud nueva (reemplaza la que sigue esperando, que se rechaza con AbortError):

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.');
  }
}

Notas: el host debe ser el propio de la página: para una app basada en un sitio web es tu dominio (el SMS que Chrome ya lee); para una app sin conexión o creada con IA es appassets.androidplatform.net. Un mensaje para otro host se ignora y la app sigue esperando. Solo en la página superior (los códigos vinculados a un origen incrustado, @top #code @iframe, no son compatibles). Solo espera una solicitud a la vez; una más nueva cancela la anterior. Evento que hay detrás: appmint:otp.

__otpCancel puente#

window.WebToApk.__otpCancel(requestId: String)

Abandona una solicitud WebOTP pendiente (AbortSignal, o una solicitud más nueva la reemplazó).

Ejemplo

Transporte interno detrás de la cancelación de una solicitud WebOTP (AbortSignal). Las páginas usan la API estándar; nunca llames a esto directamente: los métodos __ pueden cambiar sin previo aviso.

Devuelve: nada. Necesita: nada que activar.

Usa la API pública:

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

Deja de esperar en cuanto el usuario empiece a escribir el código por sí mismo:

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

Cancela la espera cuando el usuario vuelve atrás para cambiar su número de teléfono:

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

Notas: Implementado en el shell de la app (WebApiPolyfills.kt). El mismo código de página funciona sin cambios en Chrome.

__otpStart puente#

window.WebToApk.__otpStart(requestId: String)

WebOTP: espera (hasta cinco minutos) un SMS mediante la hoja SMS User Consent de Google, sin permiso de SMS. La respuesta es `appmint:otp {requestId, message|error}`.

Ejemplo

Transporte interno detrás de navigator.credentials.get({ otp: { transport: ['sms'] } }). Las páginas usan la API estándar; nunca llames a esto directamente: los métodos __ pueden cambiar sin previo aviso.

Devuelve: nada; la respuesta llega como el evento appmint:otp {requestId, message} o {requestId, error} (cancelled, timeout, unavailable). Necesita: nada que activar.

Usa la API pública:

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

Deja de esperar después de un minuto con una señal de tiempo de espera:

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.');
  }
}

Dile al usuario qué hacer a continuación según el nombre del error:

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

Notas: Implementado en el shell de la app (WebApiPolyfills.kt). El mismo código de página funciona sin cambios en Chrome.

passkeyIsAvailable puente#

window.WebToApk.passkeyIsAvailable(): Boolean

True en Android 9+ con Google Play Services: los teléfonos que pueden guardar una passkey.

Ejemplo

Te dice si este teléfono puede usar passkeys. Las páginas normalmente usan la API pública AppMint.passkey.isAvailable() (o PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()), que envuelve esta llamada.

Devuelve: un booleano, de inmediato: true en Android 9+ con Google Play Services y un WebView lo bastante reciente para el canal de passkeys. Necesita: nada.

La API pública:

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

La llamada directa, si la necesitas:

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

Ofrece «Añadir una passkey» una vez después de un inicio de sesión con contraseña, solo en los teléfonos que pueden guardar una (la comprobación estándar, el mismo código que en Chrome):

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;
}

Notas: Cuando esto es false, AppMint.passkey.create/get se rechazan con NotSupportedError. Oculta el botón de passkey y ofrece tu otro método de inicio de sesión.

passkeyOrigin puente#

window.WebToApk.passkeyOrigin(): String

El origen que ve un servidor de passkeys desde esta app: `android:apk-key-hash:<base64url SHA-256>`.

Ejemplo

Da el origen que ve tu servidor de passkeys desde esta app: android:apk-key-hash:<base64url SHA-256 of the signing certificate>. Las páginas normalmente usan la API pública AppMint.passkey.origin(), que envuelve esta llamada.

Devuelve: una cadena, de inmediato ("" si no se puede leer el certificado). Necesita: nada.

Ejecuta esto una vez en la app instalada DESDE Google Play y añade el valor a los orígenes permitidos de tu servidor:

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

La llamada directa devuelve la misma cadena:

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

Muestra el origen en una pantalla de ajustes oculta, para poder leerlo desde la instalación de Play sin un depurador:

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
}

Notas: Google Play vuelve a firmar tu app, así que un APK de prueba da un valor distinto al de la instalación de Play. Ejemplo con SimpleWebAuthn: expectedOrigin: ['https://your-domain.com', 'android:apk-key-hash:...'].

signInWithGoogle puente#

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

===== Inicio de sesión nativo con Google mediante Credential Manager ===== Evita el bloqueo de OAuth de Google en el WebView de Android (error disallowed_useragent) ejecutando el flujo de inicio de sesión en Android nativo y pasando a JS solo el token de ID de Google resultante, donde el SDK web de Firebase lo consume mediante GoogleAuthProvider.credential(idToken) + signInWithCredential(...). Contrato de JS: window.WebToApk.signInWithGoogle(serverClientId, callbackId) serverClientId "" = el cliente web (tipo 3) del google-services.json propio del creador para este paquete; si no hay ninguno, error "missing_web_client_id". Una página que no es la propia de la app recibe el error "foreign_origin". -> al terminar, se llama a window.__webToApkAuth[callbackId](resultJsonString) resultJsonString = '{"ok":true,"idToken":"...","email":"...","displayName":"..."}' o '{"ok":false,"error":"<message>"}'

Ejemplo

Inicia el inicio de sesión nativo con Google (el selector de cuentas de Android) y responde con un token de ID de Google. Las páginas normalmente usan el ayudante con Promise WebToApkAuth.signInWithGoogle(webClientId), que envuelve esta llamada.

Devuelve: nada. La respuesta llega después: el shell llama a window.__webToApkAuth[callbackId](json) con '{"ok":true,"idToken":"...","email":"...","displayName":"...","profilePictureUri":"..."}' o '{"ok":false,"error":"<code>","detail":"..."}', y luego elimina esa entrada. Necesita: tu ID de cliente web de OAuth y el SHA-1 / SHA-256 de la app registrados en tu proyecto de Google o Firebase. Sin interruptor de compilación.

El ayudante (recomendado):

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;
  }
}

La llamada en bruto (orden de los argumentos: ID de cliente web y luego el id de tu callback):

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

La llamada en bruto con un ID de cliente vacío, que usa el cliente web del google-services.json que subiste:

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.');
  });

Notas: Códigos de error: cancelled (el usuario cerró el selector), google_play_services_unavailable (no hay cuenta de Google ni proveedor; también es el aspecto de un SHA-1 sin registrar), missing_web_client_id (no se pasó ninguno y no hay cliente web en el google-services.json que subiste), foreign_origin (la página en pantalla no es la propia de la app), failed, unexpected_credential_type, parse_failed: .... Un primer argumento vacío usa el cliente web de tu propio google-services.json subido, si lo tiene. Verifica el token de ID en tu servidor (o pásalo a Firebase con GoogleAuthProvider.credential(idToken)).

signOutGoogle puente#

window.WebToApk.signOutGoogle(callbackId: String)

Borra el estado guardado de Credential Manager para que la siguiente llamada a signInWithGoogle vuelva a mostrar el selector de cuentas (en lugar de volver a seleccionar en silencio la última cuenta). Las apps generadas deben llamar a esto al cerrar sesión por iniciativa del usuario, además de firebase.auth().signOut().

Ejemplo

Borra el estado guardado de inicio de sesión con Google de Android, de modo que el siguiente inicio de sesión vuelva a mostrar el selector de cuentas. Las páginas normalmente usan el ayudante con Promise WebToApkAuth.signOutGoogle(), que envuelve esta llamada.

Devuelve: nada. La respuesta llega después: el shell llama a window.__webToApkAuth[callbackId](json) con '{"ok":true}' o '{"ok":false,"error":"..."}'. Necesita: nada.

El ayudante (recomendado):

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

La llamada en bruto:

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

La llamada en bruto envuelta en una Promise, usada cuando el usuario elimina su cuenta:

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

Notas: No cierra la sesión del usuario en Firebase ni en tu servidor: hazlo tú mismo.

Generado a partir del runtime de la app y sus archivos de ejemplo en cada compilación de la documentación. Léelo como Markdown · Todas las familias.

Comprobado con el puente publicado el 2026-10-22.