@tiledev/sdk-apptile-live-selling-native
v0.1.1
Published
The native half of Apptile live selling: the LivePip Expo module (system picture-in-picture over a Zego stream) and the config plugin that writes the native entries prebuild regenerates. Split out of @tiledev/sdk-apptile-live-selling so a JS-only release
Readme
@tiledev/sdk-apptile-live-selling-native
The native half of Apptile live selling: the LivePip Expo module — system picture-in-picture fed
from a Zego stream — and the config plugin that writes the native entries expo prebuild
regenerates.
You do not talk to this package directly — you use
@tiledev/sdk-apptile-live-selling,
which depends on it and re-exports the two things it exposes. But you do have to install it, and
name its plugin in app.json.
npm i @tiledev/sdk-apptile-live-selling @tiledev/sdk-apptile-live-selling-nativeWhy list it when the JS package already depends on it. In a tree that contains expo and
react-native, npm nests this package at
node_modules/@tiledev/sdk-apptile-live-selling/node_modules/… rather than hoisting it. Expo's
autolinking finds it there fine — but its config-plugin resolver resolves plugin names from the
project root only, so expo prebuild fails with
PluginError: Failed to resolve plugin for module "@tiledev/sdk-apptile-live-selling-native". A
direct entry is what hoists it.
Pin it to exactly the version the JS package's dependencies names — never a different one, or
npm keeps two copies of this Expo module and the identical pod and module names collide at link time:
"@tiledev/sdk-apptile-live-selling": "^0.3.0",
"@tiledev/sdk-apptile-live-selling-native": "0.1.0"Why this is a package of its own
Everything in here lands in the OTA fingerprint, and nothing in the JS package does.
Expo autolinks any package carrying an expo-module.config.json and records its packageVersion
in expoAutolinkingConfig:ios / :android — one of the content sources fingerprint.json hashes.
So any version bump of an autolinked package moves the fingerprint and needs a fresh binary in the
stores, even a release that touched nothing but JavaScript. Before the split that applied to the
whole SDK: a one-line JS fix meant a new build, a review and a staged rollout.
Now the line is drawn where it actually is. This package moves when the Swift, the Kotlin, the podspec or the plugin change — and then a new binary is the honest answer anyway. The JS package is not autolinked, so its version is not hashed and a release of it ships over the air.
The JS package pins this one exactly, so its own version is the only thing that can move this one. A patch of it is JavaScript alone and ships over the air; a minor means this package moved with it, a new binary is required, and the pin above wants updating in the same commit. That is the whole contract — the release procedure maintaining it is in the JS package's README.
The config plugin
Every native entry live selling needs lives in a file expo prebuild regenerates, so it has to be a
plugin: an edit by hand works on the machine that made it and silently stops working in CI.
{
"expo": {
"plugins": [
["@tiledev/sdk-apptile-live-selling-native", {
"cameraPermission": "Used to appear on camera when you co-host a live show."
}]
]
}
}| Prop | Default | What it writes |
|---|---|---|
| pip | true | android:supportsPictureInPicture, the configChanges a PIP resize needs, UIBackgroundModes: ["audio"] |
| cameraPermission / microphonePermission | generic copy | NSCameraUsageDescription / NSMicrophoneUsageDescription — never overwrites strings your app already sets |
| capturePermissions | true | Android CAMERA + RECORD_AUDIO |
Zego's own Maven repo is added unconditionally: im.zego:express-video lives only there, and a clean
Android checkout fails at configuration time without it.
Then npx expo prebuild and run the app. The LivePip module autolinks itself.
If your app already vendors a
live-pipmodule, delete it — identical module and pod names collide at link time.
The marker the plugin writes into android/build.gradle still reads
@generated by @tiledev/sdk-apptile-live-selling, and it has to. The plugin reads that string back
to stay idempotent, so a project prebuilt before the split carries the old one; renaming it would
make the plugin fail to recognise its own block and append a second allprojects clause.
What it exports
import { livePip, isLivePipSupported } from '@tiledev/sdk-apptile-live-selling-native';isLivePipSupported says the module is in the binary — not that the device will grant a window.
An Android handset can ship without the picture-in-picture feature and the user can revoke the
permission in settings; both surface as onPipError, because neither can be known until a window is
asked for.
livePip is the boundary itself: configure, setStream, setArmed, start, stop,
addListener. The live session drives all of it, so an app rarely calls it directly.
Two files, and Metro picks by platform: pip.native.ts is the real module, pip.ts is a no-op with
isLivePipSupported: false. TypeScript resolves the non-native one, which is why a consumer's
tsc --noEmit never needs expo installed.
Picture-in-picture, in two stages
actions.enterPip() on the live session shrinks to a surface your app draws. The session arms the
OS handover in advance, so minimising the app opens a real system window with no JavaScript involved
at that moment — which is the only way it can work, since JS is suspended by then. Coming back raises
onPipRestore.
On iOS the window is an AVPictureInPictureController fed Zego's custom-render frames, which is why
UIBackgroundModes: ["audio"] is load-bearing: without it iOS suspends the app and the window
freezes on its last frame within seconds. On Android there is no frame copy — the activity itself
enters PIP, which is why android:configChanges must include smallestScreenSize. Entering PIP is
exactly a change of smallest width, and without it the activity is recreated rather than resized: the
React host restarts on the way in, the stream drops, and the window shows a fresh launch.
Scripts
| Command | What it does |
|---|---|
| npm run build | Cleans, compiles src → dist and plugin/src → plugin/build. Both are needed: app.plugin.js loads the compiled plugin so consumers never need TypeScript at prebuild |
| npm run lint | Typechecks both projects, emitting nothing |
| npm run clean | Removes dist and plugin/build |
| npm pack | The publish tarball — use it to verify dist, ios/, android/, expo-module.config.json, app.plugin.js and plugin/build all ship |
