> ## Documentation Index
> Fetch the complete documentation index at: https://docs.craveup.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native / Expo Quickstart

> Build a mobile storefront that calls the public Crave API directly.

The Storefront API is safe to call directly from an Expo app. Published catalog reads are anonymous, cart access is scoped by a short-lived capability, and signed-in customer resources use a customer JWT. No API key or mobile BFF is required.

## 1. Create the app

```bash theme={null}
npx create-expo-app my-storefront --template blank-typescript
cd my-storefront
npx expo install expo-secure-store
npm install --save-exact @craveup/storefront-sdk@2.0.1
```

## 2. Configure public values

```env filename=".env" theme={null}
EXPO_PUBLIC_CRAVEUP_API_URL=https://api.craveup.com
EXPO_PUBLIC_CRAVEUP_LOCATION_ID=loc_your_location_id
EXPO_PUBLIC_CRAVEUP_MERCHANT_SLUG=your-restaurant-slug
```

Never put Crave admin, partner, or payment-provider secrets in an `EXPO_PUBLIC_` variable.

## 3. Create the client

```ts filename="src/lib/storefront.ts" theme={null}
import * as SecureStore from "expo-secure-store";
import {
  createStorefrontClient,
  type StorefrontCartSession,
  type StorefrontSessionStore,
} from "@craveup/storefront-sdk";

const apiUrl = process.env.EXPO_PUBLIC_CRAVEUP_API_URL;
const merchantSlug = process.env.EXPO_PUBLIC_CRAVEUP_MERCHANT_SLUG;
if (!apiUrl || !merchantSlug) throw new Error("Missing Storefront configuration");

const apiOrigin = new URL(apiUrl).origin;
const secureKeyPart = (value: string) =>
  Array.from(value, (character) =>
    character.codePointAt(0)!.toString(16).padStart(6, "0"),
  ).join("");
const environmentNamespace = secureKeyPart(apiOrigin);
const merchantNamespace = secureKeyPart(merchantSlug);
const key = (
  kind: "cart" | "customer" | "receipt",
  resourceId: string,
) =>
  [
    "storefront",
    kind,
    "v1",
    environmentNamespace,
    merchantNamespace,
    secureKeyPart(resourceId),
  ].join(".");
const cartKey = (locationId: string) => key("cart", locationId);
const customerKey = key("customer", "jwt");
const receiptKey = (receiptId: string) => key("receipt", receiptId);

async function readCartSession(
  locationId: string,
): Promise<StorefrontCartSession | null> {
  const storageKey = cartKey(locationId);
  const raw = await SecureStore.getItemAsync(storageKey);
  if (!raw) return null;

  try {
    const value = JSON.parse(raw) as Partial<StorefrontCartSession>;
    if (
      value.locationId !== locationId ||
      typeof value.cartId !== "string" ||
      typeof value.revision !== "number" ||
      !Number.isInteger(value.revision) ||
      value.revision < 0 ||
      (value.accessToken !== undefined && typeof value.accessToken !== "string")
    ) {
      throw new Error("Invalid cart session");
    }
    return value as StorefrontCartSession;
  } catch {
    await SecureStore.deleteItemAsync(storageKey);
    return null;
  }
}

export const sessionStore: StorefrontSessionStore = {
  async get(locationId) {
    return readCartSession(locationId);
  },
  async set(session) {
    await SecureStore.setItemAsync(
      cartKey(session.locationId),
      JSON.stringify(session),
    );
  },
  async clear(locationId) {
    await SecureStore.deleteItemAsync(cartKey(locationId));
  },
};

export const customerTokenStore = {
  get: () => SecureStore.getItemAsync(customerKey),
  set: (token: string) => SecureStore.setItemAsync(customerKey, token),
  clear: () => SecureStore.deleteItemAsync(customerKey),
};

export const receiptTokenStore = {
  get: (receiptId: string) =>
    SecureStore.getItemAsync(receiptKey(receiptId)),
  set: (receiptId: string, token: string) =>
    SecureStore.setItemAsync(receiptKey(receiptId), token),
  clear: (receiptId: string) =>
    SecureStore.deleteItemAsync(receiptKey(receiptId)),
};

export const storefront = createStorefrontClient({
  baseUrl: apiOrigin,
  sessionStore,
  getAuthToken: customerTokenStore.get,
});
```

## 4. Load the merchant and create a cart

```ts theme={null}
const merchant = await storefront.merchant.getBySlug(
  process.env.EXPO_PUBLIC_CRAVEUP_MERCHANT_SLUG!,
);

const locationId = process.env.EXPO_PUBLIC_CRAVEUP_LOCATION_ID!;
const session = await storefront.orderingSessions.start(locationId, {
  marketplaceId: "mobile",
  fulfillmentMethod: "takeout",
});

await storefront.cart.addItem(locationId, session.cart.id, {
  productId: "prod_margherita",
  quantity: 1,
  selections: [],
  itemUnavailableAction: "remove_item",
});
```

SecureStore keys accept only alphanumeric characters, `.`, `-`, and `_`; the example encodes every dynamic component before composing dot-delimited keys. The environment, merchant, and location scopes prevent credentials from crossing applications or deployment tiers. Store only the small cart session, merchant-scoped customer JWT, and receipt capability there—not cart contents or API caches.

Clear the guest cart capability after claim, deletion, expiry, or terminal checkout handling; retain only the returned revision when a claimed cart remains customer-authenticated. Call `customerTokenStore.clear()` on logout. Store a deep-link receipt capability with `receiptTokenStore.set(receiptId, token)` only when resume support requires it, and remove it after terminal display or expiry. If the API returns `CART_CONFLICT`, fetch the latest cart before asking the customer to retry.

Native requests normally send no browser `Origin` header, so they do not require CORS registration. If you also ship an Expo web build, register that exact browser origin with Crave; never use a wildcard workaround.

`marketplaceId` is optional attribution for the app source, such as `"mobile"`;
it is not a location ID. The route already scopes the cart to `locationId`.

For a signed-in shopper who still has a guest-capability cart, set the named
`includeCustomerContext` option to `true` on discount or payment-session calls
that need customer-aware validation or provider customer association. Omit it for
guest checkout; a claimed cart uses the customer JWT automatically.

## Next steps

<CardGroup cols={2}>
  <Card title="Manage Cart" icon="cart-shopping" href="/guides/manage-cart">
    Learn the capability and revision lifecycle.
  </Card>

  <Card title="Checkout Flow" icon="credit-card" href="/guides/checkout-flow">
    Integrate payment and authoritative order-result polling.
  </Card>
</CardGroup>

<Snippet file="need-help.mdx" />
