@ciabosoftwaresolutions/capacitor-distribution-channel
v0.1.2
Published
Capacitor plugin to detect how an app was installed — TestFlight vs App Store on iOS, Play Store vs sideloaded on Android.
Maintainers
Readme
@ciabosoftwaresolutions/capacitor-distribution-channel
Capacitor plugin to detect how an app was installed — TestFlight vs App Store on iOS, Play Store vs sideloaded on Android. One line of code, zero native setup.
If this plugin saves you time, consider buying us a coffee ☕
Why
A single build can't dynamically re-point itself at a different backend — the usual approach is building separate dev/prod variants and remembering which one to archive for TestFlight vs the App Store. That's fine, but it's manual, and it's easy to accidentally ship the wrong one.
iOS actually gives you a real, Apple-documented way to tell TestFlight and App Store installs apart at runtime: the app's receipt file path. This plugin exposes that check, plus the closest equivalent on Android.
import { DistributionChannel } from '@ciabosoftwaresolutions/capacitor-distribution-channel';
const { channel } = await DistributionChannel.getDistributionChannel();
// channel === 'testflight' | 'appstore' | 'development' | 'playstore' | 'sideload' | 'web' | 'unknown'Platform support
| | iOS | Android |
|---|---|---|
| Detection basis | App receipt path (Bundle.main.appStoreReceiptURL) | Install source (PackageManager install-source info) |
| Distinguishes | TestFlight vs App Store vs local Xcode Debug run | Google Play vs sideloaded — not which Play track |
| Accuracy | Exact — this is Apple's own documented signal | Partial — Play Store doesn't expose which release track (internal/closed/open/production) an install came from, so playstore covers all of them |
| Native setup required | None | None |
Read that last row carefully for Android: playstore does not mean "production." It means "Google Play was the installer," which is true for every testing track too. There is no Android API that tells you which track an app was installed from. If you need dev/prod switching on Android, you still need separate build variants — this plugin can't help there, and doesn't pretend to.
How it works
There's no server call, no configuration, and no native project changes on either platform — the values are read directly from OS-level signals already present on the device.
iOS App launch ──▶ Bundle.main.appStoreReceiptURL ──▶ 'testflight' | 'appstore' | 'development'
Android App launch ──▶ PackageManager install-source ──▶ 'playstore' | 'sideload'| Value | Platform | Meaning |
|---|---|---|
| testflight | iOS | Installed via TestFlight (sandboxReceipt). |
| appstore | iOS | Installed via the App Store — production. |
| development | iOS | Running directly from Xcode (Debug configuration). |
| playstore | Android | Installed via Google Play — any release track. |
| sideload | Android | Installed by anything other than Google Play (adb, a third-party store, a manually distributed APK). |
| web | Both | Running as a plain website, not inside a native shell. |
| unknown | iOS | No receipt present (rare — e.g. certain jailbreak/tamper scenarios). |
Requirements
| Tool | Minimum version |
|---|---|
| Capacitor | 8.0 |
| iOS deployment target | 13.0 |
| Android minSdkVersion | 23 |
| Node | 18+ |
Installation
npm install @ciabosoftwaresolutions/capacitor-distribution-channel
npx cap syncThat's it — no Xcode capabilities, no Info.plist keys, no AndroidManifest.xml permissions, no build.gradle edits. The plugin auto-registers on both platforms.
Usage
The most common use case — pick a backend at launch:
import { DistributionChannel } from '@ciabosoftwaresolutions/capacitor-distribution-channel';
const { channel } = await DistributionChannel.getDistributionChannel();
switch (channel) {
case 'testflight':
case 'development':
// point at your dev/staging backend
setApiBaseUrl('https://api-dev.yourapp.com');
break;
case 'appstore':
// point at production
setApiBaseUrl('https://api.yourapp.com');
break;
case 'playstore':
case 'sideload':
// Android: pick your build-time-configured environment as usual —
// this plugin cannot tell testing tracks from production here.
setApiBaseUrl(environment.apiBaseUrl);
break;
case 'web':
case 'unknown':
default:
setApiBaseUrl(environment.apiBaseUrl);
break;
}Other common uses: gating verbose logging / debug menus to non-production installs, tagging crash reports and analytics events with the install channel, or showing a "you're on the beta build" banner to TestFlight testers.
API
getDistributionChannel()
getDistributionChannel() => anyReturns how this app was installed.
iOS detection is based on the app's receipt file
(Bundle.main.appStoreReceiptURL) — Apple's own documented signal for
distinguishing a TestFlight install (sandboxReceipt) from a real App
Store install (receipt). Builds run directly from Xcode are reported
as development rather than testflight, since a Debug build also
carries a sandbox receipt path but was never actually distributed.
Android detection is based on PackageManager's install-source info,
which can only tell you whether Google Play was the installer — it
cannot tell you which release track. See DistributionChannel for details
on what each returned value means.
Returns: any
Interfaces
DistributionChannelResult
| Prop | Type |
| ------------- | ------------------------------------------------------------------- |
| channel | DistributionChannel |
Type Aliases
DistributionChannel
How the app was installed/distributed.
testflight— iOS, installed via TestFlight.appstore— iOS, installed via the App Store (production).development— iOS, running directly from Xcode (Debug configuration).playstore— Android, installed via Google Play. This covers every release track (internal, closed, open, production) — Google Play does not expose which track an install came from, so there is no Android equivalent to the iOStestflightvsappstoredistinction.sideload— Android, installed by any means other than Google Play (adb install, a third-party store, a manually distributed APK, etc).web— running as a plain website, not inside a native shell.unknown— unable to determine (e.g. no receipt present on iOS).
'testflight' | 'appstore' | 'development' | 'playstore' | 'sideload' | 'web' | 'unknown'
FAQ
Why does a TestFlight build show development in my Simulator?
The Simulator and local Xcode Debug runs are always reported as development, regardless of how the archive would ultimately be distributed — #if DEBUG is checked before the receipt is ever inspected. To see real testflight / appstore values you need a Release build installed through TestFlight or the App Store, on a physical device.
Can I tell internal testing apart from production on Android?
No — and no Capacitor or native plugin can. Google Play's PackageManager install-source API only reports the installer package (com.android.vending), not which release track served the install. If you need that distinction, use separate Android build variants/flavors configured at build time instead.
Does this require network access?
No. Everything is read locally from the OS — no request is made, so it works offline and returns instantly.
Contributing
PRs welcome! Please open an issue first for non-trivial changes.
npm install
npm run buildBefore submitting a PR, run the platform verify scripts (requires Xcode/CocoaPods for iOS, Android SDK for Android):
npm run verify:web # build
npm run verify:ios # pod install + xcodebuild
npm run verify:android # gradlew build + test
npm run verify # all threeSupport
If this plugin helped you ship faster, consider buying us a coffee — it helps keep the project maintained and free for everyone. ☕
License
MIT © Ciabo Software Solutions
