app.config.ts
Rama main · réplica de lectura del repositorio original.
import { ExpoConfig, ConfigContext } from "expo/config";
import fs from "fs";
import path from "path";
import pkg from "./package.json";
// Derives a monotonic integer from semver so `pnpm bump:*` propagates to the
// native build identifiers Google Play / App Store require. 1.2.3 → 10203.
const [major, minor, patch] = pkg.version.split(".").map((n) => parseInt(n, 10));
const nativeBuildNumber = major * 10000 + minor * 100 + patch;
// APP_VARIANT=development produces a side-by-side install: different name,
// bundle id, package, and URL scheme so both prod and dev builds can coexist
// on the same device. Slug and EAS project stay shared so OTA channels still
// route correctly (dev binaries pull the "development" channel via eas.json).
const IS_DEV = process.env.APP_VARIANT === "development";
const APP_NAME = IS_DEV ? "Dashboarr Dev" : "Dashboarr";
const BUNDLE_ID = IS_DEV ? "com.dashboarr.app.dev" : "com.dashboarr.app";
const APP_SCHEME = IS_DEV ? "dashboarr-dev" : "dashboarr";
// google-services.json holds real FCM credentials, so it's gitignored and a
// fresh clone won't have it. Local dev builds must still work: when the file
// is missing we omit android.googleServicesFile entirely, prebuild skips the
// Google Services gradle plugin, and push tokens are simply unavailable at
// runtime. Without this, a missing file aborts prebuild partway and leaves a
// broken half-generated android/ folder.
// Builds that ship must instead fail loudly — silently dropping FCM would
// break push for every user. That means any EAS Android build (the file
// arrives there via the GOOGLE_SERVICES_JSON file secret) and local release
// builds (scripts/build-android-prod.js sets DASHBOARR_RELEASE=1). iOS never
// uses this file — push goes through APNs.
const GOOGLE_SERVICES_FILE = process.env.GOOGLE_SERVICES_JSON ?? "./google-services.json";
const HAS_GOOGLE_SERVICES = fs.existsSync(
path.isAbsolute(GOOGLE_SERVICES_FILE) ? GOOGLE_SERVICES_FILE : path.join(__dirname, GOOGLE_SERVICES_FILE)
);
const IS_ANDROID_RELEASE =
process.env.DASHBOARR_RELEASE === "1" || process.env.EAS_BUILD_PLATFORM === "android";
if (!HAS_GOOGLE_SERVICES) {
if (IS_ANDROID_RELEASE) {
throw new Error(
`google-services.json not found (looked for ${GOOGLE_SERVICES_FILE}). ` +
"Release builds require real FCM credentials: place google-services.json at the " +
"project root, or point GOOGLE_SERVICES_JSON at it (EAS file secret on cloud builds)."
);
}
console.warn(
"[app.config] google-services.json not found — building without FCM. " +
"Push notifications will be unavailable in this build."
);
}
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: APP_NAME,
"owner": "dashboarr",
slug: "dashboarr",
version: pkg.version,
orientation: "portrait",
icon: "./assets/icon.png",
userInterfaceStyle: "dark",
newArchEnabled: true,
splash: {
image: "./assets/splash.png",
resizeMode: "cover",
backgroundColor: "#09090b",
},
// EAS Update — OTA JS/asset updates shipped via `eas update --branch <channel>`.
// Binary and update must share the same runtimeVersion; bumping the native
// `version` (or adding any native code) cuts a new runtime and requires a
// fresh TestFlight / Play Store build.
updates: {
url: "https://u.expo.dev/2e40d2d5-f7c5-4c28-922c-40e2a5ab2a8c",
requestHeaders: {
"expo-channel-name": process.env.EAS_UPDATE_CHANNEL ?? "production",
},
},
runtimeVersion: {
policy: "fingerprint",
},
ios: {
// iOS 18+ Home Screen appearances (Edit > Customize): iOS swaps in the
// dark or tinted variant when the user picks that look, and the tinted
// one takes whatever tint color they choose (#415). All three come from
// scripts/generate-icon.js. Native assets: needs a new binary.
icon: {
light: "./assets/icon.png",
dark: "./assets/icon-dark.png",
tinted: "./assets/icon-tinted.png",
},
supportsTablet: true,
bundleIdentifier: BUNDLE_ID,
buildNumber: String(nativeBuildNumber),
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
NSLocalNetworkUsageDescription:
"Dashboarr sends Wake-on-LAN magic packets to your home server on the local network.",
// Self-hosted services on a LAN almost always speak plain http://, and
// iOS blocks cleartext by default (App Transport Security). Mirrors the
// Android cleartext-traffic plugin. The remote-URL form already warns
// when users pick http:// for an internet-facing endpoint.
NSAppTransportSecurity: {
NSAllowsArbitraryLoads: true,
},
// Registers the app as an "Open in" target for .torrent files (Files app,
// Safari downloads, the share sheet's app row). iOS has no built-in UTI
// for torrents, so the type is imported here under the identifier the
// desktop clients (Transmission, qBittorrent) export. Opened files are
// copied into Documents/Inbox and arrive as a file:// URL, which
// app/+native-intent.ts routes to the Downloads add card. Native
// config: needs a new binary, not an OTA update.
// Declaring document types makes App Store Connect require an explicit
// open-in-place choice (ITMS-90737). NO keeps the Inbox-copy behavior
// above; YES would hand us the original file, which needs
// security-scoped access the add flow doesn't do.
LSSupportsOpeningDocumentsInPlace: false,
CFBundleDocumentTypes: [
{
CFBundleTypeName: "BitTorrent Document",
LSHandlerRank: "Alternate",
LSItemContentTypes: ["org.bittorrent.torrent"],
},
],
UTImportedTypeDeclarations: [
{
UTTypeIdentifier: "org.bittorrent.torrent",
UTTypeDescription: "BitTorrent Document",
UTTypeConformsTo: ["public.data"],
UTTypeTagSpecification: {
"public.filename-extension": ["torrent"],
"public.mime-type": ["application/x-bittorrent"],
},
},
],
},
// Required so NetInfo can read the current Wi-Fi SSID/BSSID for the
// local-vs-remote auto-switch feature. iOS also needs Location When-In-Use
// permission at runtime (handled in lib/wifi.ts via expo-location).
entitlements: {
"com.apple.developer.networking.wifi-info": true,
},
},
android: {
// Android 12+ ignores full-screen splash images — the system splash is a
// small centered icon (~288dp). Feeding it the tall splash.png shrinks the
// badge to a speck (reported on Honor Magic 6 Pro), so Android gets the
// badge-only square art instead; iOS keeps the full-screen splash above.
// scripts/generate-icon.js also writes the density drawables directly.
splash: {
image: "./assets/adaptive-icon.png",
resizeMode: "contain",
backgroundColor: "#09090b",
},
adaptiveIcon: {
foregroundImage: "./assets/adaptive-icon.png",
// Android 13+ themed icons: with the system's themed-icons toggle on,
// the launcher draws this silhouette in the wallpaper palette instead
// of the colored foreground (#415).
monochromeImage: "./assets/adaptive-icon-monochrome.png",
backgroundColor: "#09090b",
},
// Always the base id at prebuild time. The dev variant suffix (".dev") is
// applied at Gradle build time by plugins/withDevVariant — that lets one
// prebuilt android/ project produce both variants by toggling APP_VARIANT
// when invoking gradlew, without a second prebuild.
package: "com.dashboarr.app",
versionCode: nativeBuildNumber,
...(HAS_GOOGLE_SERVICES ? { googleServicesFile: GOOGLE_SERVICES_FILE } : {}),
// VIEW intent filters for .torrent files (file managers, browser
// downloads, "Open with"). The first matches by MIME type; the second
// catches providers that label the download application/octet-stream
// but keep the .torrent name (a host is required for a path pattern to
// match). The delivered content:// / file:// URI reaches
// app/+native-intent.ts through Linking. Native config: needs a new
// binary, not an OTA update.
intentFilters: [
{
action: "VIEW",
category: ["DEFAULT", "BROWSABLE"],
data: [
{ scheme: "content", mimeType: "application/x-bittorrent" },
{ scheme: "file", mimeType: "application/x-bittorrent" },
],
},
{
action: "VIEW",
category: ["DEFAULT", "BROWSABLE"],
data: [
{ scheme: "content", host: "*", mimeType: "*/*", pathPattern: ".*\\.torrent" },
{ scheme: "file", host: "*", mimeType: "*/*", pathPattern: ".*\\.torrent" },
],
},
],
},
// APP_SCHEME must stay first — expo-linking's createURL uses the first entry.
// "magnet" registers the app as a magnet-link handler (Android intent filter
// + iOS CFBundleURLSchemes); incoming URLs are rewritten by app/+native-intent.ts.
scheme: [APP_SCHEME, "magnet"],
// Shared EAS project used by every install. Required for real push tokens —
// Notifications.getExpoPushTokenAsync({ projectId }) reads from extra.eas.projectId.
// Keep this stable across releases; replace with the real EAS project UUID
// from `eas init`. Must NOT have Enhanced Security for Push Notifications
// enabled on the Expo side (see backend README).
extra: {
eas: {
"projectId": "2e40d2d5-f7c5-4c28-922c-40e2a5ab2a8c"
},
},
plugins: [
"expo-router",
"expo-secure-store",
"expo-web-browser",
"expo-localization",
"./plugins/withAndroidSigning",
"./plugins/withCleartextTraffic",
"./plugins/withDevVariant",
"./plugins/withFmtConstevalFix",
"./plugins/withGradleJvmArgs",
"./plugins/withInsecureTls",
"./plugins/withPodsDeploymentTarget",
"./plugins/withSceneLifecycle",
[
"expo-location",
{
locationWhenInUsePermission:
"Allow Dashboarr to detect your WiFi network name for automatic local/remote URL switching.",
},
],
[
"expo-notifications",
{
color: "#3b82f6",
},
],
[
"expo-camera",
{
cameraPermission: "Allow Dashboarr to scan your backend pairing QR code.",
},
],
// Android home-screen "Releasing Soon" calendar widget (issue #224). The
// widget UI + data live in widgets/*; this plugin injects the AppWidget
// <receiver> + provider metadata into the manifest. `name` MUST match
// WIDGET_NAME in widgets/widget-config.ts. updatePeriodMillis is Android's
// 30-min floor (smaller is clamped); the reliable refresh is the in-app
// foreground push (hooks/use-widget-refresh). The dev variant's ".dev"
// applicationIdSuffix doesn't rename the manifest namespace, so prod and dev
// install as two distinct providers with no plugin change.
[
"react-native-android-widget",
{
widgets: [
{
name: "Calendar",
label: "Releasing Soon",
description: "Upcoming TV episodes and movie releases",
minWidth: "250dp",
minHeight: "110dp",
targetCellWidth: 4,
targetCellHeight: 2,
updatePeriodMillis: 1800000,
resizeMode: "horizontal|vertical",
},
],
},
],
],
});