# @eazo/sdk
Capability-first SDK for web apps that run both on a standard browser and inside the Eazo Mobile WebView. Write one codebase; the SDK picks the right implementation for the runtime.
## Install
```bash
npm install @eazo/sdk
```
## Quick start
```tsx
// app/layout.tsx
import { EazoProvider } from "@eazo/sdk/react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return {children};
}
```
```tsx
// Any component
import { auth } from "@eazo/sdk";
import { useEazo } from "@eazo/sdk/react";
export function Header() {
const user = useEazo((s) => s.auth.user);
if (!user) return ;
return Hi, {user.name};
}
```
## API
### `auth`
```ts
import { auth } from "@eazo/sdk";
auth.user // User | null
auth.loading // boolean
auth.authenticated // boolean
auth.loginUIOpen // boolean (web login UI)
await auth.getToken() // string | null
auth.onChange((user) => { ... }) // () => void (unsubscribe)
// One-stop login — handles every runtime and idempotent if already signed in.
const user = await auth.login() // User
await auth.login({ timeoutMs: 120_000 }) // custom timeout (default: 5 min)
auth.showLogin() // imperative open (no await)
auth.hideLogin() // imperative close (rejects pending login())
// Low-level login primitives (rarely needed; `login()` orchestrates these).
await auth.loginWithSocial("google")
await auth.loginWithEmailPassword(email, password)
await auth.loginWithEmailCode(email, code)
await auth.sendEmailCode(email)
await auth.logout()
auth.fetchSocialConnections() // SocialConnection[]
```
#### `auth.login()` — unified login flow
`auth.login()` is the canonical way to sign a user in. It:
1. Returns the current user immediately if already authenticated (idempotent).
2. On Eazo Mobile (host advertises `auth.requestLogin`), delegates to the native host login UI.
3. Otherwise, shows the SDK-bundled login modal (social providers + email / code / password).
```tsx
```
It rejects with `DENIED` if the user cancels, or `TIMEOUT` after 5 minutes of inactivity (configurable via `timeoutMs`).
### `device`
```ts
import { device } from "@eazo/sdk";
device.platform // 'web' | 'mobile'
device.locale // 'zh-CN' | ...
device.getContext() // full DeviceContext
```
#### Edge-to-edge layout and safe areas
Eazo Mobile presents hosted apps edge to edge with `viewport-fit=cover`. On
iOS, WebKit publishes native values through `env(safe-area-inset-*)`.
`EazoProvider` does not move or pad hosted app content in Mobile: backgrounds
may remain full bleed, while visible text and controls must consume the
template's resolved `--safe-*` variables (or `env()` directly in apps that do
not use the template).
### `share`
Hand share materials (text + image attachments) to the platform's compose surface. Inside the Eazo Mobile WebView the host opens its native compose page, AI-drafts a post from the inputs, and lets the user edit and publish; in a plain browser the SDK shows a "Continue in the Eazo app" CTA pointing to https://eazo.ai/.
```ts
import { share } from "@eazo/sdk";
await share.compose({
text: "Made carbonara tonight — first time the egg didn't scramble.",
attachments: [
{
type: "image",
url: "data:image/jpeg;base64,...", // data: or https:
caption: "finished dish photo",
},
],
sourceAppId: "recipe-keeper", // optional attribution
targetPath: "/recipes/carbonara-42", // optional app-internal destination
});
// → { accepted: true } in the mobile app
// → { accepted: false } on the web (download CTA shown)
```
`attachments` currently supports image attachments only. Pass up to 4 total image materials. `images: string[]` is still accepted for legacy apps, but new code should prefer `attachments` so images can carry a short caption/meaning.
`targetPath` is optional and tells Eazo Mobile where to open the source app when someone taps the published share widget. It must be a relative path inside the source app, such as `/profile/u_123` or `/result/abc?tab=summary`; absolute URLs are rejected.
`share.compose` throws `INVALID_ARGS` synchronously if none of `text`, `attachments`, or `images` is provided, or if more than 4 total image materials are passed.
### `notifications`
Per-app push-notification subscription, mediated by the host. Calls flow through the postMessage bridge to the mobile shell which writes the per-(user, app) bit; the embedded app never talks to the platform directly here.
```ts
import { notifications } from "@eazo/sdk";
const { subscribed } = await notifications.isSubscribed();
if (!subscribed) await notifications.subscribe(); // opt the user in
// later…
await notifications.unsubscribe(); // opt the user out
```
In a plain browser (no host) every method resolves `{ subscribed: false }` — no throws — so apps can render the right UI during local web development without special-casing.
**Publishing** is a separate, server-side surface — see [`@eazo/sdk/server` notifications](#server-notifications-publish).
### React integration
```ts
import { EazoProvider, useEazo } from "@eazo/sdk/react";
```
Rule: inside React render, read reactive state via `useEazo(selector)`. Outside render (event handlers, effects, non-React code), read directly from `auth.xxx` / `device.xxx`.
```tsx
const user = useEazo((s) => s.auth.user);
const { platform, locale } = useEazo((s) => s.device);
```
### Server (Next.js route handler)
```ts
import { requireAuth } from "@eazo/sdk/server";
export function GET(req: NextRequest) {
const r = requireAuth(req);
if (!r.ok) return r.response;
// r.user: User
}
```
Requires `EAZO_PRIVATE_KEY` in the server environment.
#### `notifications.publish`
Send a system push to every user subscribed to your app. Authenticates by signing an ES256K JWT with `EAZO_PRIVATE_KEY` — there is no user JWT in scope, so this is the right path for cron-driven digests, event-driven alerts, or any backend-originated notification.
```ts
import { notifications } from "@eazo/sdk/server";
export async function POST() {
const { delivered, publishId } = await notifications.publish({
title: "Slow-cooker timer",
body: "Your stew is ready.",
data: { recipeId: "stew-42" }, // optional, surfaces in the device tap handler
});
return Response.json({ delivered, publishId });
}
```
`audience` defaults to `"subscribers"` (v1's only value).
Throws `EazoNotificationPublishError` on platform-level errors (`code` 401 = bad JWT, 403 = appId doesn't belong to your key, 413 = >5,000 subscribers, etc.).
### Payments
Eazo marketplace payments are available through the payments subpaths. Generated apps create local Next.js routes and pages, but the platform checkout/status calls stay inside the SDK.
Install a Payment SDK-compatible release, validate the dependency and lockfile,
then scaffold from that installed package:
```bash
pnpm add '@eazo/sdk@>=0.22.3'
./node_modules/.bin/eazo-sdk payments doctor --minimum-version 0.22.3
./node_modules/.bin/eazo-sdk payments init --minimum-version 0.22.3 --recipe one-time-unlock
```
Eazo-generated apps should use the platform-provided `sdk_package_spec` instead
of assuming the public npm source. `0.22.3` is a compatibility floor, not an
upper-bound pin; later compatible releases such as `0.23.x` or `0.24.x` remain
valid when the required payment exports are present.
Generated files include:
```text
src/lib/eazo-payments/catalog.ts
src/components/eazo-payments/PaymentUnlockPanel.tsx
src/app/api/payments/checkout/route.ts
src/app/api/payments/status/route.ts
src/app/api/payments/entitlements/route.ts
src/app/payment/success/page.tsx
src/app/payment/cancel/page.tsx
src/lib/eazo-payments/payment-contract.test.ts
src/lib/eazo-payments/payment-ui-contract.test.tsx
```
#### Several items in one payment
A one-time payment can also cover several line items, or more than one of the
same item (`one-time-items` recipe, `@eazo/sdk` >= 0.23.0):
```bash
./node_modules/.bin/eazo-sdk payments init --recipe one-time-items
```
Eazo charges one Stripe Checkout for the lines the app priced (up to 100 lines,
quantity 1–999, subtotal up to USD 5,000 via `getEazoItemsMaximumAmount`),
applies an optional app-computed discount, grants an entitlement for each line
with `entitlementKey`, and reports whether the payment is paid. The app writes
two functions in `lib/eazo-payments/items.ts`:
- `resolveItems({ lines, data })` prices the browser's `{ id, quantity }` lines
on the server and returns `{ currency, items, discountAmount?, discountLabel? }`,
or `null` to reject the request.
- `fulfillPayment(payment, { user })` grants the lines without
`entitlement_key`. It runs only for paid payments and must be idempotent by
`payment.payment_id`.
It uses the same routes as single-product payments: `createEazoCheckoutRoute({
resolveItems })` (with `getProduct` too if the app also sells catalog products),
`createEazoPaymentStatusRoute({ fulfill })`, `/payment/success` and
`/payment/cancel`, plus `createEazoPaymentSyncRoute({ fulfill })` at
`/api/payments/sync` with `PaymentSync` in the root layout. The browser starts
checkout with `startEazoItemsCheckout(lines, { data })`. See
`examples/payments/one-time-items`.
UI code should place the scaffolded component so checkout, login, status
polling, and entitlement state stay on the SDK-owned lifecycle:
```tsx
import {
PaymentUnlockPanel,
PremiumEntitlementGate,
} from "@/components/eazo-payments/PaymentUnlockPanel";
}
free={}
/>
```
For custom UI, edit the scaffolded panel's markup/classes/text or use
`EazoPaymentLifecycle` / `useEazoPaymentLifecycle(productKey)` from
`@eazo/sdk/payments/react`. The SDK lifecycle owns checkout, login, status
polling, errors, and entitlement refresh; app code should only render the
returned state.
Server code should use the route helpers generated by the CLI:
```ts
import { createEazoCheckoutRoute } from "@eazo/sdk/payments/next";
import { getPaymentProduct } from "@/lib/eazo-payments/catalog";
export const POST = createEazoCheckoutRoute({ getProduct: getPaymentProduct });
```
For a full copyable Next.js example, see:
```text
examples/payments/one-time-unlock/
```
That example includes product catalog constants, local route handlers,
success/cancel pages, profile/upgrade UI, and mock contract tests for checkout,
status, entitlement, and legacy-flow regression checks.
Do not add Stripe SDKs, Stripe webhooks, `STRIPE_SECRET_KEY`, direct
`/api/open/payments/*` request bodies, or localStorage-only premium state to
generated apps. Checkout uses same-window navigation, confirms payment by
polling the Eazo ledger after Stripe returns with `payment_id`, and refreshes
platform-owned entitlements before paid UI renders as active.
### Testing
```ts
import { __resetSDK, __dispatchHostMessage } from "@eazo/sdk/testing";
afterEach(() => __resetSDK());
```
## Types
```ts
interface User {
id: string;
email: string | null;
name: string | null;
avatarUrl: string | null;
}
interface DeviceContext {
platform: "web" | "mobile";
locale: string;
}
```
## How it works
The SDK talks to the Eazo Mobile host over `postMessage` using the protocol documented in [PROTOCOL.md](./PROTOCOL.md). When no host responds within 1.5 seconds, the SDK falls back to web-native implementations (GenAuth for login, `localStorage` for session, `navigator.language` for locale).
App code never branches on environment — the capability API is the same on both platforms.
## Environment
| Variable | Required | Used by |
|---|---|---|
| `EAZO_APP_ID` | yes | App ID; auto-resolved by ``. |
| `EAZO_API_BASE` | yes | Eazo platform base; payment server helpers derive the Creator `/creator` base from it. |
| `EAZO_PLATFORM_API_BASE` | optional | Override the Eazo platform base URL (defaults to `https://eazo.ai`). |
| `EAZO_PRIVATE_KEY` | server | `requireAuth`, `notifications.publish`, payment checkout/status routes |