# SDKs

> The JavaScript client for servers, and the Swift (StoreKit 2) and Kotlin (Play Billing) SDKs for apps.

## JavaScript and TypeScript

A dependency-free client for servers and edge runtimes, plus webhook verification.

```sh
npm install @salesdogg/sdk
```

```ts
import { createClient } from "@salesdogg/sdk";

const sd = createClient({ apiKey: process.env.SALESDOGG_SECRET_KEY! });

await sd.hasEntitlement(user.id, "pro");                 // boolean
await sd.entitlements(user.id);                          // active entitlements
await sd.setAttributes(user.id, { $email: user.email });
const { url } = await sd.createCheckout({ app_user_id: user.id, product_id: "prod_…", success_url: "https://example.com/ok" });
const { data } = await sd.listCustomers({ entitlement: "pro", country: "DE" });
```

Errors are thrown as `SalesDoggError` with the API's `status`, `type`, `code` and `hint`. Set `apiUrl` for a local or self-hosted API.

## Mobile SDKs

Native SDKs for Apple platforms and Android share one small API and behave the same way:

- **Anonymous first.** Without a user id the SDK creates `$anon:<uuid>` and keeps it across launches. `identify(userId)` moves anything bought anonymously to the user; `signOut()` starts a new anonymous id.
- **The server decides access.** After a purchase the SDK sends the store's proof to `POST /v1/receipts`; entitlements come back from the server, never from the device.
- **No lost purchases.** If recording fails (offline, server down) the purchase is retried on the next launch.
- **Works offline.** The last known entitlements are cached and returned with `isStale = true` when the server is unreachable.
- **Background changes.** Renewals, Ask to Buy approvals and pending payments that clear are published on the entitlement stream.

Both use the **public key** (`sd_pk_…`).

### Swift (iOS, macOS, tvOS, watchOS)

StoreKit 2, Swift concurrency, no dependencies. iOS 15+, macOS 12+, tvOS 15+, watchOS 8+. Add the package with Swift Package Manager and pick the `SalesDogg` library.

```swift
import SalesDogg

SalesDogg.configure(publicKey: "sd_pk_...")

let offering = try await SalesDogg.shared.offerings().current
switch try await SalesDogg.shared.purchase(offering!.packages[0]) {
case .purchased(let entitlements): unlock(entitlements.isActive("pro"))
case .pending: showPendingNotice()
case .cancelled: break
}

try await SalesDogg.shared.identify(user.id)   // after your own sign-in
```

Server side: an `app_store` app with your bundle id, and products registered with their App Store product ids. If your user ids are lowercase UUIDs, the SDK also passes them to the App Store as `appAccountToken`, so notifications are attributed even if the app is killed mid-purchase.

### Kotlin (Android)

Google Play Billing Library 9 and coroutines, minSdk 23. Two modules: `salesdogg-core` (pure Kotlin) and `salesdogg` (Play Billing).

```kotlin
import com.salesdogg.sdk.*
import com.salesdogg.sdk.android.*

SalesDogg.configure(this, publicKey = "sd_pk_...")   // Application.onCreate

val offering = SalesDogg.shared.offerings().current
when (val outcome = SalesDogg.shared.purchase(activity, offering!!.packages.first())) {
    is PurchaseOutcome.Purchased -> unlock(outcome.entitlements.isActive("pro"))
    PurchaseOutcome.Pending -> showPendingNotice()
    PurchaseOutcome.Cancelled -> Unit
}

lifecycleScope.launch { SalesDogg.shared.entitlementUpdates.collect { render(it) } }
```

Server side: a `play_store` app with your package name and service account. Purchases are acknowledged by the SalesDogg server, so the app does not call `acknowledgePurchase`. Consumable products are not supported yet.

### Coming from RevenueCat

Pass RevenueCat's current app user id to `configure` the first time only. An anonymous `$RCAnonymousID:` id is treated as anonymous, so that user's purchases follow them when they sign in.
