npm package: @retroselfie/api-client
Description: Expo-friendly TypeScript client for Retroselfie API v2.
Latest version: 1.0.17

# `@retroselfie/api-client`

Expo-friendly TypeScript client for calling NaughtySpirit `/api/v2/*` endpoints.

## Install

```bash
npm i @retroselfie/api-client
```

If you’re in an Expo app, you should still install native dependencies with `expo install` (to match your SDK versions):

```bash
npx expo install expo-secure-store expo-auth-session
```

## Quick start (anonymous + user)

```ts
import { createApiClient } from "@retroselfie/api-client";

const api = createApiClient({
  baseUrl: "https://yourdomain.com",
  apiKey: "your-api-key",
  deviceId: "device-id"
});

await api.auth.bootstrapAnonymous();
const profile = await api.user.get();
```

`bootstrapAnonymous()` only calls `POST /api/v2/auth/anon` when **no** session is stored yet. If the user already has tokens in SecureStore (e.g. after login or a previous anonymous bootstrap), it **reuses** that session so a second call on app resume/reload does not replace a logged-in user with a new anonymous user.

`api.wallet` is deprecated; use `api.user` instead (same underlying `GET /api/v2/wallet` response).

## Paywall (v2)

These endpoints require bearer auth, so call `auth.bootstrapAnonymous()` (or login/signup/SSO) first.

```ts
await api.auth.bootstrapAnonymous();
const paywall = await api.paywall.get();
// paywall.products -> use rcProductId to fetch localized pricing via RevenueCat on-device
```

## Feedback (flag image)

```ts
await api.feedback.flagImage({
  reason: "inappropriate",
  imageId: "abc123",
  imageUrl: "https://..."
});
```

## Email/password signup

`POST /api/v2/auth/signup` accepts `email` and `password`. You can optionally link an anonymous session (from `POST /api/v2/auth/anon`) by sending `anonymousRefreshToken` and, if you want, `anonymousUserId`.

- **Email only (no anonymous user):** call `signupWithEmailPassword` with no third argument and no stored session, or ensure you are not reusing an anonymous session you did not intend to merge.
- **After `bootstrapAnonymous()`:** the client sends `anonymousRefreshToken` (and `anonymousUserId` when it matches the stored session) from the current session automatically.
- **Explicit tokens:** pass `SignupWithEmailOptions` when the anon session is not the one in storage.

```ts
// Email/password only (no anonymous link)
await api.auth.signupWithEmailPassword("you@example.com", "password");

// After anonymous bootstrap — links anonymous → registered user
await api.auth.bootstrapAnonymous();
await api.auth.signupWithEmailPassword("you@example.com", "password");

// Or pass tokens explicitly
await api.auth.signupWithEmailPassword("you@example.com", "password", {
  anonymousRefreshToken: "<from POST /api/v2/auth/anon>",
  anonymousUserId: "<optional; must match anon user id>"
});
```

## Email/password login + deduct credits

By default, `loginWithEmailPassword` sends only `{ email, password }`. The logged-in account uses whatever wallet is already linked on the server — the client does **not** attach the pre-login anonymous session automatically (that would incorrectly merge the anon wallet when the account already has its own wallet).

You can optionally pass `anonymousRefreshToken` / `anonymousUserId` only if you explicitly want a merge-on-login flow; tokens are **not** read from storage for login.

```ts
await api.auth.loginWithEmailPassword("you@example.com", "password");
await api.credits.deduct(1);
```

## Forgot password

1. **`requestPasswordReset(email)`** — `POST /api/v2/auth/forgot-password` with `{ email }`. If your server accepts a post-reset redirect for app deep links, pass `{ next: Linking.createURL("auth/reset-done") }` as the second argument.
2. **`resetPassword(token, newPassword)`** — `POST /api/v2/auth/reset-password` with `{ token, password }`, where `token` is the value from the reset link (or whatever your API documents). If the response includes a full session (same shape as login), the client stores it and returns it; otherwise it returns `undefined` and the user can sign in with the new password.

```ts
import * as Linking from "expo-linking";

await api.auth.requestPasswordReset("you@example.com");

// Optional: help the server build a redirect back into the app
await api.auth.requestPasswordReset("you@example.com", {
  next: Linking.createURL("auth/reset-done")
});

const session = await api.auth.resetPassword(tokenFromEmail, "new-password");
if (!session) {
  await api.auth.loginWithEmailPassword("you@example.com", "new-password");
}
```

## OAuth (Expo) + deep link redirect

Your server's `GET /api/v2/auth/oauth/callback` supports either:
- redirecting to `next` and appending `accessToken`, `refreshToken`, `expiresAt`, `userId` query params, or
- returning `200` JSON with those same fields.

As of the latest server changes, `/api/v2/auth/oauth/callback` requires an app key via `X-API-Key` (API client requests) or `appKey` query param (browser redirects). This client always sends `X-API-Key`, and the `/api/v2/auth/oauth/start` endpoint will ensure browser redirects include `appKey` automatically.

In Expo, set up a scheme (e.g. `myapp://`) and pass your deep link as `next`.

```ts
import * as Linking from "expo-linking";

const next = Linking.createURL("auth/callback");
await api.auth.startOAuth("google", { next });
```

## Multiple `createApiClient()` instances

If different parts of the app (e.g. a root provider and a modal) each call `createApiClient()` with the **default** storage, they share the same Expo SecureStore keys. The client re-reads the session from storage on every authenticated request, so after login/signup/OAuth on one instance, another instance’s next request uses the new tokens—you do not need to recreate the client.

If you pass a **custom** `storage`, ensure all clients that must share session state use the **same** `TokenStorage` instance (for example a module-level `MemoryTokenStorage` in tests).

## Notes
- `api.wallet` and the `WalletResponse` type are deprecated in favor of `api.user` and `UserResponse`.
- The client automatically retries once after a single in-flight refresh if a request returns `401`.
- You can provide your own token storage via `storage` in `createApiClient` config.
- Paywall responses do not include localized pricing; use `rcProductId` with RevenueCat on-device.