| name | spectacles-commerce |
| description | Reference guide for CommerceKit and In-App Purchases on Spectacles — covering CommerceKitModule initialization, converting prices to integer cents, editor mock purchases versus device production purchases, queryPurchaseHistory for ownership checking, launchPurchaseFlow to prompt a swipe-to-buy, acknowledgePurchase to verify consumption, and handling UserCanceled responses. Use this skill when building premium lenses, freemium features, unlocking levels, or selling non-consumable digital items on the Snap platform. |
Spectacles CommerceKit — Reference Guide
CommerceKit enables handling in-app purchases (IAP) for non-consumable digital items directly inside Custom Lenses on Spectacles.
Note: Commerce capabilities are in Closed Beta and require your Lens Studio account to have proper onboarding.
Setup & Architecture
Commerce uses the CommerceKitModule. You define a local "Product Catalog" representing the items you are selling.
Prices in CommerceKit APIs are always expected in Integer Cents (e.g. $2.99 is passed as 299).
1. Creating the Commerce Client
Initialize the client with your catalog. You must provide a callback to handle asynchronous purchase results.
const commerceModule = require("LensStudio:CommerceKitModule") as CommerceKitModule
let commerceClient: Commerce.Client
const catalog = [
{ id: "premium_skin_01", displayName: "Gold Skin", price: { currency: "USD", price: 299 } },
{ id: "level_pack_02", displayName: "Extra Levels", price: { currency: "USD", price: 499 } }
]
const clientOptions = new Commerce.ClientOptions((result, purchases) => {
onPurchasesUpdated(result, purchases)
})
clientOptions.extras = JSON.stringify({ productCatalog: catalog })
commerceClient = commerceModule.createClient(clientOptions)
commerceClient.startConnection()
.then(async () => {
print("Commerce connection started.")
await refreshOwnership()
})
.catch((e) => print("Commerce connection failed: " + e))
2. Checking Ownership History
Before offering a purchase, query the user's history to see if they already own it.
const ownedProducts = new Map<string, Commerce.PurchaseState>()
async function refreshOwnership() {
const history = await commerceClient.queryPurchaseHistory()
ownedProducts.clear()
history.forEach((purchase) => {
ownedProducts.set(purchase.productId, purchase.purchaseState)
print(`Product ${purchase.productId} is ${purchase.purchaseState}`)
})
}
function isProductOwned(productId: string): boolean {
return ownedProducts.get(productId) === Commerce.PurchaseState.Purchased
}
3. Launching the Purchase Flow
When a user taps a "Buy" button, attempt to launch the flow.
async function buyProduct(productId: string) {
if (global.deviceInfoSystem.isEditor()) {
print(`Mock purchase completed for ${productId} (Editor mode)`)
ownedProducts.set(productId, Commerce.PurchaseState.Purchased)
return
}
if (isProductOwned(productId)) {
print("User already owns this.")
return
}
const result = await commerceClient.launchPurchaseFlow(productId)
if (result.responseCode !== Commerce.ResponseCode.Success) {
print("Failed to launch flow: " + result.responseCode)
}
}
4. Handling the Purchase Callback
The ClientOptions callback receives results from the system purchase overlay. A purchase isn't finalized until you acknowledge it.
async function onPurchasesUpdated(result: Commerce.PurchaseResult, purchases: Commerce.Purchase[]) {
if (!purchases || purchases.length === 0) return
switch (result.responseCode) {
case Commerce.ResponseCode.Success:
for (const p of purchases) {
if (p.token) await finalizePurchase(p.token)
}
break
case Commerce.ResponseCode.UserCanceled:
print("User cancelled the purchase flow.")
break
default:
print(`Purchase error: Code ${result.responseCode}`)
break
}
await refreshOwnership()
}
Acknowledging the Purchase
async function finalizePurchase(token: string) {
try {
const ackResult = await commerceClient.acknowledgePurchase(token)
if (ackResult.responseCode === Commerce.ResponseCode.Success) {
print("Purchase verified and acknowledged successfully!")
}
} catch (err) {
print("Failed to acknowledge purchase: " + err)
}
}
Common Gotchas
- Cent conversion: Always multiply floating-point dollars by 100 and use
Math.round() prior to passing to CommerceKit to avoid floating point drift. (e.g. Math.round(2.99 * 100)).
- Editor Mode: Commerce flows do not launch in Lens Studio editor. Mock your purchases by checking
global.deviceInfoSystem.isEditor() and faking an immediate "Owned" state.
- Pending States:
queryPurchaseHistory might return Commerce.PurchaseState.Pending if network failed halfway. You should attempt to re-acknowledge pending items on startup.
Reference Examples