FrankAPI

v0.4.15
SDK shipped

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.

Why FrankAPI instead of window.FrankenApp?
Raw window.FrankenApp is the low-level bridge. FrankAPI is the layer you should write code against.
  • Browser-safe. Every method no-ops with { ok: false, reason: "not_native" } outside the shell. Never throws.
  • Shape-normalised. Older shells return { success: true }, newer ones return { ok: true }. SDK hides this from you.
  • Guards built in. 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.
  • Forward-compatible. oauth.signIn, camera.takePhoto, files.pick etc. are already on the surface and will light up when the shell ships them. No rewrite.
  • Safer debugging. FrankAPI.debug.snapshot() is safe to paste into support tickets - it shows bridge presence + method availability but NEVER token values.
Install in 3 steps
Step 1. Add the script to index.html
<script src="https://frankenapp.com/frankapi.js"></script>
Step 2. Create 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;
Step 3. Import and use
import { FrankAPI } from "@/lib/frankapi";
if (FrankAPI.native.isAvailable()) FrankAPI.haptics.success();
Method reference
Hover the info icons for context on each method.
Detection
CallReturns
FrankAPI.native.isAvailable()
boolean
FrankAPI.native.platform()
"ios" | "android" | "tvos" | "web"
FrankAPI.native.bridgeVersion()
string | null
Auth (Supabase) - PREFERRED
CallReturns
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 }
Biometric (low-level)
CallReturns
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 }
Haptics
CallReturns
FrankAPI.haptics.impact('light' | 'medium' | 'heavy')
{ ok }
FrankAPI.haptics.success()
{ ok }
FrankAPI.haptics.error()
{ ok }
Share + Clipboard
CallReturns
FrankAPI.share.open({ title?, text?, url? })
{ ok }
FrankAPI.clipboard.write(text)
{ ok }
FrankAPI.clipboard.read()
{ ok, raw }
Reserved (coming soon)
CallReturns
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" }
How-to guides
Confirmed-working patterns. Copy and adapt.

Migrate an existing app from window.FrankenApp
For apps that wired Face ID / haptics / share against the raw bridge (GMMC and earlier).

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.