@tiledev/sdk-apptile-cart-sync
v0.2.1
Published
Shared-cart sync ("Cart Assist") for TilePacket apps — reports which cart a device is on and adopts the customer's shared cart across devices and the web store. Coordinates with @apptile/sdk-shopify and @tiledev/sdk-apptile-cart-hold.
Readme
@tiledev/sdk-apptile-cart-sync
The shopper's cart, shared — across their devices and the web store. (This is the platform's "Cart Assist"; the package is named for what it does.)
Apptile keeps a pointer to a customer's cart on the customer record, so the same cart can be seen and edited elsewhere — a support agent in the dashboard, or the shopper on the web store, which reads the identical metafield. This SDK reconciles the two directions:
up — report which cart this device is on (makes the backend write the pointer)
down — when the pointer names a different cart, adopt it, folding this device's cart in firstnpm install @tiledev/sdk-apptile-cart-sync@apptile/sdk-shopify is a peer (the two coordinate on the same cart). react is a peer of the
main entry, which re-exports the triggers; react-native is only ever a type.
Signed-in shoppers only — the shared cart hangs off the customer. Every step degrades to "keep the local cart": this is a convenience layer, and a shopper losing their cart to a failed sync is far worse than a sync that quietly did nothing.
Usage
import AsyncStorage from "@react-native-async-storage/async-storage";
import { configureCartSync } from "@tiledev/sdk-apptile-cart-sync";
const cartSync = configureCartSync({
config: { enabled: true, apiUrl, mergeApiUrl },
appId, // Apptile ENGINE app id (shared with Cart Hold)
shop: storeDomain,
customerAccountRequest, // host's authed Customer Account API transport
getAccessToken, // customer access token, for the report header
storage: AsyncStorage, // remembers locally-checked-out carts
});
await cartSync.sync({
cartId, // this device's cart GID
resolveCustomerId, // async → numeric customer id (guest ⇒ null ⇒ no-op)
adopt, // switch to a shared cart GID
refresh, // re-read the current cart in place
reset, // abandon a spent cart, start fresh
notify, // shopper-facing toast
});
// On checkout:
await cartSync.markCheckedOut(cartId, numericCustomerId);React triggers
Everything ships from the package root, hooks included. There is no ./react subpath: one is
reachable only through the exports map, which a resolver that ignores it (node10, some test runners
and bundlers) cannot see at all.
import { useCartSync, useCartSyncCheckedOut } from "@tiledev/sdk-apptile-cart-sync";
function Root() {
const { cart, adopt, refresh, reset } = useCart(); // from @apptile/sdk-shopify
const { isLoggedIn } = useAccount();
const { ready } = useShopify();
const { sync } = useCartSync({
client: cartSync, isLoggedIn, ready,
cartId: cart?.id ?? null,
resolveCustomerId, adopt, refresh, reset, notify,
});
// Attach `sync` to screen focus / app foreground yourself (host owns react-navigation & AppState).
}The hook syncs on sign-in, on the SDK becoming ready, and on the cart id changing; it returns a stable
sync() for the focus/foreground triggers, which stay in the host so this package imports neither
react-navigation nor react-native.
Surface
@tiledev/sdk-apptile-cart-sync
| Export | Notes |
| --- | --- |
| configureCartSync(options) | Builds and remembers the session client. Returns a CartSyncClient. |
| getCartSyncClient() | The configured client, or null. |
| CartSyncClient | Class: .sync(args), .reportCart(), .markCheckedOut(), .recordCheckedOut(). |
| toCartToken · toCartGid · toNumericCustomerId · isMergeable | Pure id helpers. |
| CART_SYNC_NAMESPACE · ACTIVE_CART_KEY · CHECKED_OUT_CARTS_KEY · POINTER_QUERY | The metafield pointer + its query. |
| DEFAULT_MESSAGES | Shopper copy for ADOPTED / SPENT. |
| type CartSyncOptions, CartSyncConfig, SyncCartArgs, SharedCart, KeyValueStorage, … | Config + wire types. |
React triggers — the same root entry
| Export | Notes |
| --- | --- |
| useCartSync(options) | Mount-once triggers; returns { sync } for focus/foreground. |
| useCartSyncCheckedOut(client, resolveCustomerGid) | Returns (cartId) => markCheckedOut. |
Config shape
interface CartSyncConfig {
enabled: boolean;
apiUrl: string; // app-proxy on apptile-server: POST /update, GET /clear
mergeApiUrl: string; // server-side cart merge
}
interface CartSyncOptions {
config: CartSyncConfig;
appId: string; // Apptile engine app id → x-shopify-app-id
shop: string; // Shopify store domain
customerAccountRequest: <T>(query, variables) => Promise<T>;
storage?: KeyValueStorage; // locally-recorded spent carts
getAccessToken?: () => Promise<string | null>;
onError?: (error, context?) => void;
fetch?: typeof fetch;
timeoutMs?: number; // default 8000
debug?: boolean;
}
interface SyncCartArgs {
cartId: string | null;
resolveCustomerId: () => Promise<string | null>;
adopt: (cartGid: string) => Promise<unknown | null>;
refresh: () => Promise<unknown>;
reset: () => Promise<void>;
notify?: (message: string) => void;
}On the web
Metro resolves a web stand-in for the two triggers, so useCartSync and useCartSyncCheckedOut do
nothing in a browser build. Cart Assist reports which device a cart is on and adopts the shared cart
in its place; running that from a preview would move the pointer on the shopper's real customer
record. The client is untouched — configureCartSync and CartSyncClient are plain fetch, so a host
that means to report from a browser can still drive them directly.
Wire protocol
| Call | Endpoint | Body |
| --- | --- | --- |
| report cart | POST {apiUrl}/update | { cartId: token, customerId, customerAccessToken } |
| mark checked out | GET {apiUrl}/clear?cid={customerId}&shop={domain} | — |
| merge carts | POST {mergeApiUrl} | { cartId1: gid, cartId2: gid, shop } |
| read pointer | Customer Account GraphQL | metafields in Apptile-CartAssist (smart_cart_id, smart_cart_checkedout_ids) |
Cart tokens on the wire keep their ?key= query — Shopify cannot read or mutate a cart without it.
Customer ids are the numeric id, not the GID.
Design notes
Kept clear of Cart Hold. Cart Sync works on cart identity; @tiledev/sdk-apptile-cart-hold works on
lines. Nothing here adds or removes a line — adoption is a read plus a state swap, and the merge is
server-side — so no write races Cart Hold's guard, and a line arriving by merge/adoption already
carries whatever _cart_hold_expiry_time its author gave it. Claims for a cart we then leave are not
released here; those units moved into the adopted cart or are still in the manager's ledger, whose
sweep releases them.
One in-flight run. Focus, foreground, sign-in and cart-change all trigger the sync and overlap constantly; the client coalesces concurrent callers into a single run, and guards the report and the announcement so an adopted cart is neither re-reported nor re-announced.
Self-contained. @apptile/sdk-shopify is not imported at all — the React hook receives the cart
controls (adopt/refresh/reset) as arguments, and every other dependency (config, storage,
customer-account transport, access token, fetch) is injected through CartSyncOptions. tsc passes
with nothing installed.
