FrankAPI is the customer-facing SDK for FrankenApp. One stable surface for Face ID, haptics, share, clipboard, and more. Safe in browsers (every method no-ops) and survives shell version changes so you never have to rewrite native feature code.
<meta name="frankapi-project-id" content="..."> to index.html - the install will appear in FrankAPI Management within seconds.{ ok: false, reason: "not_native" } outside the shell. Never throws.{ success: true }, newer ones return { ok: true }. SDK hides this from you.biometric.enable requires a non-empty token, auth.enableBiometricLogin validates via the current auth session, and auth.unlockWithBiometrics has a re-entry guard so one tap cannot create a prompt loop.oauth.signIn, camera.takePhoto, files.pick etc. are already on the surface and will light up when the shell ships them. No rewrite.FrankAPI.debug.snapshot() is safe to paste into support tickets - it shows bridge presence + method availability but NEVER token values.index.html<script src="https://frankenapp.com/frankapi.js"></script>
src/lib/frankapi.ts// src/lib/frankapi.ts
// Single import point for the SDK. The stub keeps your app rendering during
// SSR / SSG when window.FrankenAppSDK has not attached yet.
const stub: any = {
version: "0.0.0-stub",
native: { isAvailable: () => false, platform: () => "web", bridgeVersion: () => null },
biometric: {
isAvailable: async () => ({ ok: true, available: false }),
isEnrolled: async () => ({ ok: true, enrolled: false }),
enable: async () => ({ ok: false, reason: "not_native" }),
unlock: async () => ({ ok: false, reason: "not_native" }),
disable: async () => ({ ok: false, reason: "not_native" }),
},
auth: {
enableBiometricLogin: async () => ({ ok: false, reason: "not_native" }),
offerBiometricLogin: async () => ({ ok: false, reason: "not_native" }),
unlockWithBiometrics: async () => ({ ok: false, reason: "not_native" }),
disableBiometricLogin: async () => ({ ok: true }),
isBiometricLoginEnabled: async () => ({ ok: true, enabled: false }),
attach: () => ({ ok: true }),
detach: () => ({ ok: true }),
signOut: async () => ({ ok: true, scope: "local" }),
},
haptics: { impact: async () => ({ ok: false, reason: "not_native" }), success: async () => ({ ok: false, reason: "not_native" }), error: async () => ({ ok: false, reason: "not_native" }) },
share: { open: async () => ({ ok: false, reason: "not_native" }) },
clipboard: { write: async () => ({ ok: false, reason: "not_native" }), read: async () => ({ ok: false, reason: "not_native" }) },
debug: { enable: () => {}, disable: () => {}, isOn: () => false, snapshot: () => ({ isNative: false }) },
};
export const FrankAPI =
(typeof window !== "undefined" ? (window as any).FrankenAppSDK : null) ?? stub;import { FrankAPI } from "@/lib/frankapi";
if (FrankAPI.native.isAvailable()) FrankAPI.haptics.success();| Call | Returns | |
|---|---|---|
FrankAPI.native.isAvailable() | boolean | |
FrankAPI.native.platform() | "ios" | "android" | "tvos" | "web" | |
FrankAPI.native.bridgeVersion() | string | null |
| Call | Returns | |
|---|---|---|
FrankAPI.auth.enableBiometricLogin({ supabase, reason? }) | { ok, enrolled?, autoAttachOk?, reason?, debug?, biometricSupabaseLoginFullySupported? } | |
FrankAPI.auth.offerBiometricLogin({ supabase, promptMessage?, force? }) | { ok, enrolled?, skipped?, reason? } | |
FrankAPI.auth.unlockWithBiometrics({ supabase, reason? }) | { ok, userId?, autoAttachOk?, reason?, debug? } | |
FrankAPI.auth.disableBiometricLogin() | { ok } | |
FrankAPI.auth.isBiometricLoginEnabled() | { ok, enabled } |
| Call | Returns | |
|---|---|---|
FrankAPI.biometric.isAvailable() | { ok, available } | |
FrankAPI.biometric.isEnrolled() | { ok, enrolled } | |
FrankAPI.biometric.enable({ userId, token, reason }) | { ok, verified, reason? } | |
FrankAPI.biometric.unlock({ reason }) | { ok, userId, token, reason? } | |
FrankAPI.biometric.disable() | { ok } |
| Call | Returns | |
|---|---|---|
FrankAPI.haptics.impact('light' | 'medium' | 'heavy') | { ok } | |
FrankAPI.haptics.success() | { ok } | |
FrankAPI.haptics.error() | { ok } |
| Call | Returns | |
|---|---|---|
FrankAPI.share.open({ title?, text?, url? }) | { ok } | |
FrankAPI.clipboard.write(text) | { ok } | |
FrankAPI.clipboard.read() | { ok, raw } |
| Call | Returns | |
|---|---|---|
FrankAPI.oauth.signIn({ provider }) reserved | { ok: false, reason: "reserved" } | |
FrankAPI.camera.takePhoto() reserved | { ok: false, reason: "reserved" } | |
FrankAPI.files.pick() reserved | { ok: false, reason: "reserved" } | |
FrankAPI.location.getCurrent() reserved | { ok: false, reason: "reserved" } | |
FrankAPI.badge.set(n) / .clear() reserved | { ok: false, reason: "reserved" } |
One-shot replacement map:
// BEFORE (low-level, customer manages refresh_token)
const r = await FrankAPI.biometric.enable({ userId: user.id, token: session.refresh_token, reason });
// ...
const u = await FrankAPI.biometric.unlock({ reason });
if (u.ok && u.token) await supabase.auth.setSession({ refresh_token: u.token, access_token: "" });
// AFTER (high-level, no refresh_token in customer code)
await FrankAPI.auth.enableBiometricLogin({ supabase, reason });
await FrankAPI.auth.offerBiometricLogin({ supabase, promptMessage: "Turn on Face ID for this app?" });
await FrankAPI.auth.unlockWithBiometrics({ supabase, reason });
await FrankAPI.auth.disableBiometricLogin();
await FrankAPI.auth.isBiometricLoginEnabled();
// Raw-bridge -> FrankAPI low-level mapping (still available, but prefer auth.*):
window.FrankenApp?.isNative === FrankAPI.native.isAvailable()
window.FrankenApp.biometric.isAvailable() === (await FrankAPI.biometric.isAvailable()).available
window.FrankenApp.isBiometricEnrolled() === (await FrankAPI.biometric.isEnrolled()).enrolled
window.FrankenApp.enableBiometric({userId,token,reason}) === await FrankAPI.biometric.enable({userId,token,reason})
window.FrankenApp.tryBiometricUnlock({reason}) === await FrankAPI.biometric.unlock({reason})
window.FrankenApp.disableBiometric() === await FrankAPI.biometric.disable()
window.FrankenApp.haptics.impact('medium') === FrankAPI.haptics.impact('medium')
window.FrankenApp.haptics.notification('success') === FrankAPI.haptics.success()
window.FrankenApp.share({title,text,url}) === FrankAPI.share.open({title,text,url})
window.FrankenApp.clipboard.write(text) === FrankAPI.clipboard.write(text)After migration, search the codebase for window.FrankenApp and window.Franken and confirm zero matches outside src/lib/frankapi.ts. Delete any ok === true || success === true normalisation - the SDK does that. Delete any "token length check" guards - the SDK rejects invalid tokens before they reach the bridge.
migrate_to_frankapi_sdk readiness prompt (visible in your project's Integration Toolkit) to drop the full migration instructions into your web app's AI chat in one paste.