# Integrating the shared mask catalog The optional SDK modules provide discovery, previews, permission-based downloads, verified opaque materials and an account-scoped installation cache. Every app uses the same SimplyBeautyKit catalog and IDs. The host owns its UI, prices, purchases, account/tenant permissions and independent privacy switch. No price, currency or wallet code belongs in the SDK. **Rendering boundary:** these modules do not yet provide a complete face-mask renderer through the C++ engine. BestBee retains its existing current-frame lower-face geometry, pixel compositor and opaque/drop fallback. Another app must provide equivalent rendering and frame synchronization. A verified material alone does not track a face or guarantee that it stays concealed. ## Dependencies | Platform | Current source module | Minimum | |---|---|---| | Android | `com.simplybeauty.mask` in `android/:lib` | Android API 24; JVM 17 build target | | Swift | Separate `ios/MaskCatalog` package, product `SimplyBeautyMaskCatalog` | Swift tools 5.9; iOS 15 or macOS 12 | The catalog capabilities are `privacy_texture_rgba8_v1` for legacy RGBA packs and `privacy_texture_png_v2` for compressed PNG packs, with SDK version 0.2.0. Use current source or newly built artifacts containing these modules; older 0.2.0 AARs do not necessarily contain them. Do not assume Maven publication. The root `SimplyBeauty` Swift product/XCFramework does **not** include the new catalog module. Add the separate package explicitly, for example: ```swift .package(path: "../SimplyBeautyKit/ios/MaskCatalog") ``` Add the `SimplyBeautyMaskCatalog` library product to the consuming app target. Swift uses Foundation/CryptoKit. Android uses platform networking/JSON APIs and does not add a coroutine dependency; the host needs INTERNET permission for remote discovery. Both bundle the three defaults and can display them offline. Configure/publish the service before expecting its default HTTPS endpoints to work; source integration alone does not establish a live deployment. ## First launch and UI 1. Prepare SDK resources away from camera/UI rendering. Immediately show `pearl` (02), `blue_porcelain` (11) and `slate` (07) with local `BundledMasks` previews. These remain included and free under the host's policy. 2. Show cached discovery, refresh metadata asynchronously, then load only visible thumbnails. Opening the app/picker must never call a full-pack download. 3. Merge host access/price state by stable ID. Unknown authorization means unavailable acquisition, not a free purchase. Keep names/order from the catalog; do not hardcode the initial optional collection. 4. On Download, complete the host's free claim or confirmed purchase, obtain a narrow permission grant, then call the SDK downloader. Show progress/retry. 5. Apply only the returned verified material after explicit selection. Download completion is not selection or permission to enable masking. Keep the current verified mask while networking runs. Retired installed styles remain usable; preserve them even when absent from discovery. Bound thumbnail concurrency. Unfetched previews cannot appear on a first offline launch. Current portable preview limits are 512×512 and 256 KiB; unsupported entries are skipped. Legacy format 1 carries 128×128 RGBA8888. Format 2 carries a 256×256, 8-bit RGB PNG in an `opaque_png` material with `png_base64`, `width` and `height` fields; pack `schema_version` must match the catalog's `pack.format_version`. Both formats require every decoded alpha byte to be 255 and limit the complete JSON bundle to 1 MiB. Format 2 rejects transparent, animated and interlaced PNGs and validates dimensions before image decoding. Decode/cache materials away from UI and camera processing; rendering uses the resulting immutable RGBA pixels, without decoding images per frame. ## Android APIs ```kotlin import com.simplybeauty.mask.* import java.io.File // Blocking API: use a host background executor or Dispatchers.IO. val client = MaskCatalogClient( cacheDirectory = File(context.filesDir, "mask-packs"), accountScope = "yourapp:$tenantId:$accountId", ) val initial = client.cachedCatalog() val refreshed = client.refreshCatalog() // metadata only; failure retains cache val previewFile = client.preview(item) // optional thumbnail only // Only after an explicit download action and host acquisition/confirmation: val material = client.download(item, MaskAuthorizationProvider { requested -> // Host backend call; never return the app's general session token. MaskDownloadGrant(downloadToken, expiresAtEpochSeconds) }) { received, total -> /* post progress to the host UI */ } val rgba = material.rgba() // verified opaque RGBA8888, defensive copy val width = material.width // 128 for format 1; 256 for format 2 val height = material.height ``` `item`, token and expiry above are supplied by the chosen catalog entry and host authorization adapter. Return errors/cancellation from that adapter; the SDK does not ask users to pay. Do not block the UI waiting for authorization. Use `BundledMasks.catalog.items` and `BundledMasks.preview(id)` for local defaults. Merge included IDs first with `cachedCatalog().items` and `installedItems()`, de-duplicating by ID. `installed(id)` returns a verified material. Android has no SDK selection property: the host stores the ID and updates its renderer. `refreshCatalog()` does not automatically refresh installed statuses on Android. Call `refreshStatus(id)` for installed optional IDs, then inspect `publicationState(id)`/`installed(id)`. `removeInstalled(id)` removes local bytes, not ownership. Switch the renderer to Slate before removing its active material. Call `close()` on logout/account change and create a client with the new scope. It disconnects active requests and invalidates pending results. Individual download cancellation currently uses the same close/recreate lifecycle; there is no Android `cancel(id)` method. Guard late UI/application callbacks with a host session generation so an old completed result cannot reach the new account. ## Swift APIs ```swift import SimplyBeautyMaskCatalog let manager = MaskPackManager(configuration: .init(cacheDirectory: privateFolder)) try await manager.setAccountScope("yourapp:\(tenantID):\(accountID)") let initial = await manager.catalog() // included + cached/installed entries let refreshed = try await manager.refreshCatalog() // metadata + installed status let preview = try await manager.preview(for: item) // Only from the user's explicit download action: let material = try await manager.download(item.id, authorization: { requested in // Host adapter handles acquisition/confirmation, then obtains permission. MaskDownloadAuthorization(bearer: downloadToken, expiresAt: expiryDate) }, progress: { phase in /* transfer progress to MainActor */ }) try await manager.select(item.id) // material selection; no privacy toggle let selected = await manager.selectedMaterial() // Hand selected.pixels/width/height to the host's privacy compositor. ``` `MaskPackManager` is an actor. Use structured tasks/`await`, never a camera callback. `cancel(id)` cancels a download; `setAccountScope` cancels downloads, invalidates old results, restores that scope's installations and selects Slate. Reset the host renderer at the same boundary; guard late UI callbacks with the host session generation. Other APIs are `installedMasks()`, `material(for:)`, `installFromPath(_:item:)`, `removeDownloaded(_:)`, `refreshInstalledStatus()` and `revokedMasks()`. Removing the selected material is refused; select Slate first. Use `BundledMasks.preview(id)` for offline defaults. A custom `MaskTransport` must preserve bounded reads, cancellation and HTTPS origin restrictions, and must never redirect bearer grants. The default uses an ephemeral URLSession. ## Renderer, cache and authorization responsibilities Keep bytes in app-private storage. Supply an application-support/files directory when downloads should survive ordinary cache eviction. Catalog/previews may be shared; installations and host entitlement state are account scoped. Scope strings should include app, tenant and account, never a session token. Replace pixels on an opaque lower-face surface computed from the **same retained camera frame**. Convert/cache the material format once on selection, outside the frame loop. Never apply an old pose to a newer raw frame. Tracking/frame failure must retain opaque full coverage or drop output. Missing materials use Slate without turning privacy off. Mode/account changes invalidate queued frames. The SDK validates bytes; the host backend establishes ownership and signs grants. Follow the [permission contract](../cloudflare/mask-catalog/schema/CONTRACT.md). BestBee's optional 10-rose policy is not imposed on another integrating app. Only an explicit `revoked` status disables installed optional masks; retirement does not. Refresh installed status with discovery, persist known revocations and switch the host renderer to Slate when necessary. Unknown/404/offline failures retain prior state. Offline devices cannot learn new revocations; downloaded assets cannot be remotely erased by a service status change. See [service deployment/publication](MASK_CATALOG_SERVICE.md). These APIs do not establish BytePlus parity, physical-device coverage quality or a completed C++ mask rendering abstraction.